| 创建时间: | 2021/11/23 14:52 |
| 更新时间: | 2022/6/9 17:53 |
| 作者: | Chris |
| 来源: | file:///C:/Users/13622314539/AppData/Local/Youdao/YNote/markdown/index.html |
文件上传aws时如果不设置文件内存长度,则
PutObjectRequest会将文件全部加载到内存中来,


https://www.jianshu.com/p/06da38e0c239
问题
默认的"Cookie",, "Set-Cookie","Authorization"会当做敏感信息头部不返回出去
解决
- 可以通过修改配置来调整屏蔽的敏感信息头部,甚至可以不屏蔽敏感信息头部,全都返回,因为默认是有三个敏感信息头部的,所以需要手动设置来改变这个默认配置
- 可以自己指定要屏蔽的response header,如果不想屏蔽,都要返回,那直接不写具体的内容即可,但是必须要增加这个配置
yml
zuul:
sensitive-headers:
properties
zuul.sensitive-headers=
/**
* 通过测试发现,当使用 double 或者 float 这些浮点数据类型时,会丢失精度,String、int 则不会
* 所以,在涉及到精度计算的过程中,我们尽量使用 String 类型来进行转换。
* <p>
* 88
* 8.8
* 8.800000000000000710542735760100185871124267578125
*/
@Test
public void test() {
BigDecimal bigDecimal = new BigDecimal(88);
System.out.println(bigDecimal);
bigDecimal = new BigDecimal("8.8");
System.out.println(bigDecimal);
bigDecimal = new BigDecimal(8.8);
System.out.println(bigDecimal);
}
| 创建时间: | 2022/5/23 11:25 |
| 更新时间: | 2022/6/7 20:08 |
| 作者: | Chris |
| 来源: | https://github.com/alibaba/fastjson2/releases/tag/2.0.1 |
Jackson 的核心模块由三部分组成
jackson-core,核心包, 提供基于"流模式"解析的相关 API,它包括 JsonPaser 和 JsonGenerator, Jackson内部实现正是通过高性能的流模式 API 的 JsonGenerator 和 JsonParser 来生成和解析 json。jackson-annotations,注解包,提供标准注解功能。jackson-databind ,数据绑定包, 提供基于"对象绑定" 解析的相关 API ( ObjectMapper ) 和"树模型" 解析的相关 API (JsonNode);基于"对象绑定" 解析的 API 和"树模型"解析的 API 依赖基于"流模式"解析的 API。<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
jackson-databind 依赖 jackson-core 和 jackson-annotations,当添加 jackson-databind 之后, jackson-core 和 jackson-annotations 也随之添加到 Java 项目工程中。在添加相关依赖包之后,就可以使用 Jackson。
Jackson 最常用的
API就是基于"对象绑定" 的ObjectMapper。
@Test
public void test() throws IOException {
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.set..
//序列化
String jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
//反序列化
user = mapper.readValue(jsonString, User.class);
}
ObjectMapper 通过
writeValue系列方法将 java 对象序列化为 json,并将 json 存储成不同的格式:String(writeValueAsString)、Byte Array(writeValueAsBytes)、Writer、File、OutStream、DataOutput。
ObjectMapper 通过
readValue系列方法从不同的数据源:String、Byte Array、Reader、File、URL、InputStream中反序列化为 java 对象。
在调用 writeValue 或调用 readValue 方法之前,往往需要设置 ObjectMapper 的相关配置信息。这些配置信息应用 java 对象的所有属性上。
示例如下
//在反序列化时忽略在 json 中存在但 Java 对象不存在的属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
//在序列化时日期格式默认为 yyyy-MM-dd'T'HH:mm:ss.SSSZ
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false)
//在序列化时忽略值为null的属性
mapper.setSerializationInclusion(Include.NON_NULL);
//忽略值为默认值的属性
mapper.setDefaultPropertyInclusion(Include.NON_DEFAULT);
Jackson ObjectMapper提供了三种方法转换
writeValue()
writeValueAsString()
writeValueAsBytes()
ObjectMapper objectMapper = new ObjectMapper();
Car car = new Car();
car.brand = "BMW";
car.doors = 4;
objectMapper.writeValue(new FileOutputStream("data/output-2.json"), car);
objectMapper.writeValue(new File("data/output-2.json"), car);
String json = objectMapper.writeValueAsString(car);
byte[] jsonBytes = objectMapper.writeValueAsBytes(car);
从json字符串读取
ObjectMapper objectMapper = new ObjectMapper();
String carJson = "{ \"brand\" : \"Mercedes\", \"doors\" : 5 }";
Car car = objectMapper.readValue(carJson, Car.class);
从json Reader读取
ObjectMapper objectMapper = new ObjectMapper();
String carJson = "{ \"brand\" : \"Mercedes\", \"doors\" : 4 }";
Reader reader = new StringReader(carJson);
Car car = objectMapper.readValue(reader, Car.class);
从json文件读取
ObjectMapper objectMapper = new ObjectMapper();
File file = new File("data/car.json");
Car car = objectMapper.readValue(file, Car.class);
从json网络文件地址读取
ObjectMapper objectMapper = new ObjectMapper();
URL url = new URL("file:data/car.json");
Car car = objectMapper.readValue(url, Car.class);
从流中读取
ObjectMapper objectMapper = new ObjectMapper();
InputStream input = new FileInputStream("data/car.json");
Car car = objectMapper.readValue(input, Car.class);
从json字节数组中读取
ObjectMapper objectMapper = new ObjectMapper();
String carJson = "{ \"brand\" : \"Mercedes\", \"doors\" : 5 }";
byte[] bytes = carJson.getBytes("UTF-8");
Car car = objectMapper.readValue(bytes, Car.class);
转换为数组
String jsonArray = "[{\"brand\":\"ford\"}, {\"brand\":\"Fiat\"}]";
ObjectMapper objectMapper = new ObjectMapper();
Car[] cars = objectMapper.readValue(jsonArray, Car[].class);
转换为集合
String jsonArray = "[{\"brand\":\"ford\"}, {\"brand\":\"Fiat\"}]";
ObjectMapper objectMapper = new ObjectMapper();
List<Car> cars = objectMapper.readValue(jsonArray, new TypeReference<List<Car>>(){});
转换为Map
String jsonObject = "{\"brand\":\"ford\", \"doors\":5}";
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMap = objectMapper.readValue(jsonObject, new TypeReference<Map<String,Object>>(){});
Jackson 默认会将
java.util.Date对象转换成long值,同时也支持将时间转换成格式化的字符串
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
objectMapper.setDateFormat(dateFormat);
String json = objectMapper.writeValueAsString(对象);
Jackson 也提供了树模型(tree model)来生成和解析 json。
若想修改或访问 json 部分属性,树模型是不错的选择。树模型由 JsonNode 节点组成
String carJson = "{ \"brand\" : \"Mercedes\", \"doors\" : 5 }";
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode node = objectMapper.readValue(carJson, JsonNode.class);
JsonNode brandNode = node.get("brand");
String brand = brandNode.asText();
System.out.println("brand = " + brand);
JsonNode doorsNode = node.get("doors");
int doors = doorsNode.asInt();
System.out.println("doors = " + doors);
JsonNode array = node.get("owners");
JsonNode jsonNode = array.get(0);
String john = jsonNode.asText();
System.out.println("john = " + john);
JsonNode child = node.get("nestedObject");
JsonNode childField = child.get("field");
String field = childField.asText();
System.out.println("field = " + field);
} catch (IOException e) {
e.printStackTrace();
}
@Test
public void testJsonNode() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readValue(JSON_STR, JsonNode.class);
String firstName = node.get("firstName").asText();
JsonNode phoneNumbers = node.get("phoneNumbers");
Iterator<JsonNode> elements = phoneNumbers.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
String type = next.get("type").asText();
String number = next.get("number").asText();
log.info("type:{}, number:{}", type, number);
}
}
[main] INFO com.chris.hutool.json.JackSonTest - type:iPhone, number:0123-4567-8888
[main] INFO com.chris.hutool.json.JackSonTest - type:home, number:0123-4567-8910
JsonParser 提供很多方法来读取 json 信息, 如 isClosed(), nextToken(), getValueAsString()等。
若想单独创建 JsonParser,可以通过 JsonFactory() 的 createParser。
String carJson = "{ \"brand\" : \"Mercedes\", \"doors\" : 5 }";
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(carJson);
Car car = new Car();
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
if(JsonToken.FIELD_NAME.equals(jsonToken)){
String fieldName = parser.getCurrentName();
System.out.println(fieldName);
jsonToken = parser.nextToken();
if("brand".equals(fieldName)){
car.brand = parser.getValueAsString();
} else if ("doors".equals(fieldName)){
car.doors = parser.getValueAsInt();
}
}
}
System.out.println("car.brand = " + car.brand);
System.out.println("car.doors = " + car.doors);
也可以通过
Reader,InputStream,URL,byte array或char array来创建JsonParser
JsonGenerator 有多种 write 方法以支持生成复杂的类型的 json,比如 writeArray,writeTree 等 。若想单独创建 JsonGenerator,可以通过 JsonFactory() 的 createGenerator。
JsonFactory factory = new JsonFactory();
JsonGenerator generator = factory.createGenerator(new File("data/output.json"), JsonEncoding.UTF8);
generator.writeStartObject();
generator.writeStringField("brand", "Mercedes");
generator.writeNumberField("doors", 5);
generator.writeEndObject();
generator.close();
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.5</version>
</dependency>
/*
* Java对象转xml
*/
@Test
public void testGenXml(){
XmlMapper xmlMapper = new XmlMapper();
Book book = new Book("Think in Java",100);
try {
String xmlStr = xmlMapper.writeValueAsString(book);
System.out.println(xmlStr);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
/*
* xml转Java对象
*/
@Test
public void testGenObjByXml(){
XmlMapper xmlMapper = new XmlMapper();
String xmlStr = "<Book><name>Think in Java</name><price>100</price></Book>";
try {
Book book = xmlMapper.readValue(xmlStr, Book.class);
System.out.println(book);
} catch (IOException e) {
e.printStackTrace();
}
}
| 创建时间: | 2022/6/7 17:54 |
| 更新时间: | 2022/6/7 18:59 |
| 作者: | Chris |
| 来源: | https://baijiahao.baidu.com/s?id=1710052002582381873&wfr=spider&for=pc |
配置分页插件:拦截对象
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
public IPage<VO> getxxxByPage(PageBean pageBean, VO queryVO) {
Page<VO> page = new Page<>(pageBean.getCurrent(), pageBean.getSize());
return tenantInfoMapper.getTenantByList(page, queryVO);
}
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
IPage<VO> getxxx(Page<VO> page, @Param("vo") TenantVO
queryVO);

private <B, T, P extends PageBean> List<B> buildPage(P p, Page<T> result, List<B> beans) {
int current = Integer.parseInt(String.valueOf(p.getCurrent()));
int size = Integer.parseInt(String.valueOf(p.getSize()));
int fromIndex = (current - 1) * size;
int toIndex = size * current;
if (toIndex > beans.size()) {
toIndex = beans.size();
}
List<B> pageBeans = beans.subList(fromIndex, toIndex);
double pages = Math.ceil((double) beans.size() / (double) size);
result.setPages((long) pages).setTotal(beans.size());
return pageBeans;
}
| 创建时间: | 2022/6/7 16:35 |
| 更新时间: | 2022/6/7 17:19 |
| 作者: | Chris |
| 来源: | http://loadhtml/ |
limit ${startPos},${pageSize}
<select parameterType="map" resultType="dayu">
select * from user
<if test="startPos!=null and pageSize!=null">
limit ${startPos},${pageSize}
</if>
</select>
@Test
public void selectUser() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
//这里塞值
Map<String,Object> parms = new HashMap<>();
parms.put("startPos","0");
parms.put("pageSize","5");
List<User> users = mapper.getUserInfo1(parms);
for (User map: users){
System.out.println(map);
}
session.close();
}

总结:
limit 0,10;
0 代表从第0条数据开始
10 代表查10条数据
等到第二页的时候就是 limit 10,10;
RowBounds 帮我们省略了limit的内容,我们只需要在业务层关注分页即可!无须再传入指定数据!
这个属于逻辑分页,即实际上sql查询的是所有的数据,在业务层进行了分页而已,比较占用内存,而且数据更新不及时,可能会有一定的滞后性!不推荐使用!
RowBounds对象有2个属性,offset和limit。
- offset: 起始行数
- limit:需要的数据行数
- 因此,取出来的数据就是:从第offset+1行开始,取limit行
@Test
public void selectUserRowBounds() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// List<User> users = session.selectList("com.dy.mapper.UserMapper.getUserInfoRowBounds",null,new RowBounds(0, 5));
List<User> users = mapper.getUserInfoRowBounds(new RowBounds(0,5));
for (User map: users){
System.out.println(map);
}
session.close();
}
List<User> getUserInfoRowBounds(RowBounds rowBounds);
<select resultType="dayu">
select * from user
</select>

官方GitHub地址:
https://github.com/pagehelper/Mybatis-PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.7</version>
</dependency>
配置MyBatis核心配置文件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
@Test
public void selectUserPageHelper() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 3);
List<User> list = mapper.getUserInfo();
// 用PageInfo将包装起来
PageInfo page = new PageInfo(list);
for (User map: list){
System.out.println(map);
}
System.out.println("page:---"+page);
session.close();
}

//第一种,RowBounds方 sqlSe式的调用
List<User> list =ssion.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<User> list = userMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));
拓展
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
| 创建时间: | 2022/5/31 14:00 |
| 更新时间: | 2022/6/7 17:16 |
| 来源: | https://mp.weixin.qq.com/s/Cx9TjVHLILsb1l8srBQFQQ |
| 创建时间: | 2022/6/7 15:28 |
| 来源: | https://mp.weixin.qq.com/s/6GNVRtIRN4RHswRVYHQsMQ |
| 创建时间: | 2020/9/2 15:31 |
| 更新时间: | 2022/6/6 10:40 |
| 作者: | Chris |
jdbc -> dbutils -> JdbcTemplate
框架是一个整体解决方案,如何进行事务控制,如何实现查询缓存,字段映射
https://my.oschina.net/jallenkwong/blog/4476789#h2_6
https://blog.csdn.net/u011863024/article/details/107854866
半自动化的ORM框架
以配置文件的形式手动编写SQL,非常灵活, 而将参数设置,预编译,执行,封装结果全部自动化
SQL和代码分开,实现业务与数据的解耦
https://github.com/mybatis/mybatis-3
https://mybatis.org/mybatis-3/
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://192.168.140.127:3306/chris?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT"/>
<property name="username" value="root"/>
<property name="password" value="65536"/>
</dataSource>
</environment>
</environments>
<!-- 将我们写好的sql映射文件(EmployeeMapper.xml)一定要注册到全局配置文件(mybatis-config.xml)中 -->
<mappers>
<mapper resource="./mapper/EmployeeMapper.xml"/>
<mapper resource="./mapper/EmployeeServiceMapper.xml"/>
</mappers>
</configuration>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.employeemapper">
<!--
namespace:名称空间;通常指定为接口的全类名
id:唯一标识
resultType:返回值类型
#{id}:从传递过来的参数中取出id值
public Employee getEmpById(Integer id);
-->
<select id="getEmpById" resultType="com.mybatis.entity.Employee">
select id, last_name lastName, email, gender
from employee
where id = #{id}
</select>
</mapper>
package com.mybatis.entity;
import lombok.Data;
@Data
public class Employee {
private int id;
private String lastName;
private String email;
private String gender;
}
根据全局配置文件得到SqlSessionFactory;
使用SqlSessionFactory,获取到sqlSession对象使用他来执行增删改查 一个sqlSession就是代表和数据库的一次会话,用完关闭
使用sql的唯一标识来告诉MyBatis执行哪个sql。sql都是保存在sql映射文件中的。
private SqlSessionFactory getSqlSessionFactory() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
return new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void test() throws IOException {
// 获取sqlSession实例,能直接执行已经映射的sql语句
// sql的唯一标识:statement Unique identifier matching the statement to use.
// 执行sql要用的参数:parameter A parameter object to pass to the statement.
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
// 加上namesapce com.mybatis.employeemapper 可以防止调用同名方法时冲突
Employee employee = openSession.selectOne("com.mybatis.employeemapper.getEmpById", 1);
System.out.println(employee);
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
package com.mybatis.dao.mapper;
import com.mybatis.entity.Employee;
public interface EmployeeMapper {
Employee getEmployeeById(int id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mybatis 支持namespace与接口名称绑定 -->
<mapper namespace="com.mybatis.dao.mapper.EmployeeMapper">
<select id="getEmployeeById" resultType="com.mybatis.entity.Employee">
select id, last_name lastName, email, gender
from employee
where id = #{id}
</select>
</mapper>
/**
* 通过接口绑定来查询数据
*
* @throws IOException
*/
@Test
public void test02() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.getEmployeeById(1);
System.out.println(employee.getClass());
System.out.println(employee);
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
结果
class com.sun.proxy.$Proxy6
Employee(id=1, lastName=chris, email=sss@1734.com, gender=m)
全局配置中标签需要按照此顺序编写
properties
settings
typeAliases
typeHandlers
objectFactory
objectWrapperFactory
reflectorFactory
plugins
environments
databaseIdProvider
mappers
mybatis使用properties标签来引入外部properties配置文件的内容
resource 指向类路径下的资源文件
url 引用网络路径或本地磁盘上的资源文件
建properties文件
mybatis-chris-helloworld-2/src/main/:/dbconfig
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.140.127:3306/chris?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
jdbc.username=root
jdbc.password=65536
改全局配置文件
mybatis-chris-helloworld-2/src/main/:/mybatis-config.xml
<configuration>
<properties resource="dbconfig.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 将我们写好的sql映射文件(EmployeeMapper.xml)一定要注册到全局配置文件(mybatis-config.xml)中 -->
<mappers>
<mapper resource="./mapper/EmployeeMapper.xml"/>
<mapper resource="./mapper/EmployeeServiceMapper.xml"/>
</mappers>
</configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<!--开启表中下划线字段A_COLUMN转JavaBean驼峰字段aColumn-->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<!--指定字段值为null时的JDBC type,默认为OTHER-->
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
别名处理器,可以为Java类型起别名, 且别名不区分大小写
在全局配置中添加
<typeAliases>
<!--
type:为Java类型的全类名,没有设置别名的情况下默认别名为类名全小写, com.mybatis.entity.Employee的默认别名为employee
alias: 为Java类型指定别名
-->
<typeAlias type="com.mybatis.entity.Employee" alias="emp"/>
<!--
为当前包以及子包里面的类起一个默认的别名
批量起别名时如果别名重复,可以使用@Alias("employeeAlias")为Java类指定新的别名
-->
<package name="com.mybatis.entity"/>
</typeAliases>
业务类
package com.mybatis.entity;
import lombok.Data;
import org.apache.ibatis.type.Alias;
@Data
@Alias("employeeAlias")
public class Employee {
private int id;
private String lastName;
private String email;
private String gender;
}
在SQL映射文件中指定
<!-- 测试默认别名, 别名不区分大小写 Employee和employ都可以-->
<select id="testDefaultAlias" resultType="Employee">
select *
from employee
where id = #{id}
</select>
<!-- 测试指定别名emp, 别名不区分大小写 Emp和emp都可以 -->
<select id="testSpecifiedAlias" resultType="Emp">
select *
from employee
where id = #{id}
</select>
<!-- 在类上使用@Alias("employeeAlias")指定别名 -->
<select id="testSpecifiedAlias" resultType="employeeAlias">
select *
from employee
where id = #{id}
</select>
处理Java类型与数据库表字段类型相互转换
NOTE Since version 3.4.5, MyBatis supports JSR-310 (Date and Time API) by default.
private List<Object> templateStickerImg;
<result column="quality_accessories" property="qualityAccessories" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
MyBatis allows you to intercept calls to at certain points within the execution of a mapped statement. By default, MyBatis allows plug-ins to intercept method calls of:
可以用来配置多种环境
org.apache.ibatis.session.Configuration
this.typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
this.typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
this.typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
this.typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
this.typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
<environment id="development">
<!-- 有两种类型的事务控制 JDBC|MANAGED , org.apache.ibatis.session.Configuration-->
<transactionManager type="JDBC"/>
<!--
数据源
type:
UNPOOLED:不使用连接池的数据源
POOLED:使用边境池的数据源
JNDI:
自定义数据源:实现DataSourceFactory接口-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
用来白支持多数据库厂商,得到不同数据库厂商的标识,mybatis跟据不同厂商的标识执行不同的SQL
DB_VENDOR - 会通过 DatabaseMetaData#getDatabaseProductName() 返回的字符串进行设置。由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短
this.typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
mybatis-chris-helloworld-2/src/main/:/mybatis-config.xml
<!--
用来支持多数据库
type: DB_VENDOR, 厂商标识由驱动自带
-->
<databaseIdProvider type="DB_VENDOR">
<!--
为不同的数据库厂商标识起别名
name:数据库厂商标识
value:别名
-->
<property name="MYSQL" value="mysql"/>
<property name="Oracle" value="oralce"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
mybatis-chris-helloworld-2/src/main/:/mapper/Employee2Mapper.xml
<!-- 在指定的mysql上数据库上执行,databaseId为databaseIdProvider的别名 -->
<select id="testSpecifiedAlias" resultType="employeeAlias" databaseId="mysql">
select *
from employee
where id = #{id}
</select>
<!-- 在指定的oralce上数据库上执行,databaseId为databaseIdProvider的别名 -->
<select id="testSpecifiedAlias" resultType="employeeAlias" databaseId="oralce">
select *
from employees
where id = #{id}
</select>
将写好的sql映射文件(EmployeeMapper.xml)注册到全局配置文件(mybatis-config.xml)中
mapper 单个注册
resource: 注册类路径下的资源文件
<mappers> <mapper resource="./mapper/EmployeeMapper.xml"/> <mapper resource="./mapper/Employee2Mapper.xml"/> </mappers>url: 注册网络路径或本地磁盘上的资源文件
<mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> <mapper url="file:///var/mappers/BlogMapper.xml"/> </mappers>class: 注册接口
将SQL映射文件与接口入在同一类路径下,并且与接口名称一致
<mappers> <mapper resource="com.mybatis.dao.mapper.EmployeeMapper"/> </mappers>不用SQL映射文件,基于注解的SQL映射, 不建议,因为会使代码和SQL耦合,使业务和数据逻辑混乱
package com.mybatis.dao.mapper; import com.mybatis.entity.Employee; import org.apache.ibatis.annotations.Select; public interface EmployeeAnnotationMapper { @Select("select * from employee where id = #{id}") Employee getEmployeeById(int id); }<mappers> <mapper class="com.mybatis.dao.mapper.EmployeeAnnotationMapper"/> </mappers>
批量注册
<mappers>
<!-- 批量注册
name:接口所在的包路径
适用于通过注解实现SQL映射的接口,对于通过配置文件实现的SQL映射,需要将XML文件与接口放在一起,否则无法批量注册
-->
<package name="com.mybatis.dao.mapper"/>
</mappers>
cache – Configuration of the cache for a given namespace.
cache-ref – Reference to a cache configuration from another namespace.
resultMap – The most complicated and powerful element that describes how to load your objects from the database result sets.
parameterMap – Deprecated! Old-school way to map parameters. Inline parameters are preferred and this element may be removed in the future. Not documented here.
sql – A reusable chunk of SQL that can be referenced by other statements.
insert – A mapped INSERT statement.
update – A mapped UPDATE statement.
delete – A mapped DELETE statement.
select – A mapped SELECT statement.
mybatis 自动封装了增删改的返回结果
int 和 long 表示影响的行数
boolean 表示是否执行成功
void表示什么都不返回
业务接口
package com.mybatis.dao.mapper;
import com.mybatis.entity.Employee;
public interface EmployeeMapper {
Employee getEmployeeById(int id);
int addEmployee(Employee employee);
int deleteEmployeeById(int id);
int updateEmployee(Employee employee);
}
SQL映射文件
<mapper namespace="com.mybatis.dao.mapper.EmployeeMapper">
<select id="getEmployeeById" resultType="com.mybatis.entity.Employee">
select *
from employee
where id = #{id}
</select>
<!--parameterType: 可以省略-->
<insert id="addEmployee" parameterType="com.mybatis.entity.Employee">
insert into employee (last_name, email, gender)
values (#{lastName}, #{email}, #{gender})
</insert>
<update id="updateEmployee">
update employee t
set t.last_name=#{lastName},
t.email=#{email},
gender=#{gender}
where t.id = #{id}
</update>
<delete id="deleteEmployeeById">
delete
from employee
where id = #{id}
</delete>x
</mapper>
修改SQL映射文件
<!--
parameterType: 可以省略
useGeneratedKeys: 设置为true,使用自增主键策略获取主键
keyProperty: 指定对应的主键属性,即当获取主键后将值封装给JavaBean中的哪个属性
-->
<insert id="addEmployee" parameterType="com.mybatis.entity.Employee" useGeneratedKeys="true" keyProperty="id">
insert into employee (last_name, email, gender)
values (#{lastName}, #{email}, #{gender})
</insert>
测试代码
@Test
public void addEmployee() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee("Cano", "Cano@gmail.com", "F");
employeeMapper.addEmployee(employee);
System.out.println("add user id:" + employee.getId());
openSession.commit();
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
改SQL映射文件
<!--
使用Oracle Sequence添加员工记录前获取ID
-->
<insert parameterType="com.mybatis.entity.Employee" database>
<!--
keyProperty: 指定对应的主键属性,即当获取主键后将值封装给JavaBean中的哪个属性
order: BEFORE 当前SQL在插入SQL之前运行
AFTER 当前SQL在插入SQL之后运行
resultType: 查出的数据返回类型
-->
<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
select EMPLOYEES_SEQ.nextval from dual
</selectKey>
<!-- 插入时的主键是从序列中拿到 -->
insert into employee (id, last_name, email, gender)
values (#{id}, #{lastName}, #{email}, #{gender})
</insert>
<!--
使用Oracle Sequence添加员工记录后获取ID
-->
<insert parameterType="com.mybatis.entity.Employee" database>
<!--
keyProperty: 指定对应的主键属性,即当获取主键后将值封装给JavaBean中的哪个属性
order: BEFORE 当前SQL在插入SQL之前运行
AFTER 当前SQL在插入SQL之后运行
resultType: 查出的数据返回类型
-->
<selectKey keyProperty="id" order="AFTER" resultType="Integer">
select EMPLOYEES_SEQ.currval from dual
</selectKey>
<!-- 插入时的主键是从序列中拿到 -->
insert into employee (id, last_name, email, gender)
values (EMPLOYEES_SEQ.nextval, #{lastName}, #{email}, #{gender})
</insert>
#{参数名}
单个参数Mybatis不作特殊处理
Mybatis会将参数封装成Map,key从 param1~paramN
#{参数名} 是从Map中取到指定key的值
第一个参数: #{param1}
第二个参数: #{param2}
命名参数
需要在接口中的方法参数上使用@Param("lastName")
参数会被封装成Map,key是@Param注解中指定的值
取值#{lastName}
Employee getEmployeeByIdAndName2(@Param("id") int id, @Param("lastName") String lastName);
<select id="getEmployeeByIdAndName2" resultType="com.mybatis.entity.Employee">
select *
from employee
where id = #{id}
and last_name = #{lastName}
</select>
POJO
如果多个参数是业务字段,可以通过传POJO,通过#{业务字段名称}来取值
Map
如果多个参数不是业务字段,可以将多个参数封装为Map, 通过#{key}来取值
<select id="getEmployeeByMap" resultType="com.mybatis.entity.Employee">
select *
from employee
where id = #{p_id}
and last_name = #{p_lastName}
</select>
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("p_id", 1);
paramMap.put("p_lastName", "Chris");
Employee employee = employeeMapper.getEmployeeByMap(paramMap);
openSession.commit();
TO(Transfer Object)
如果多个参数不是业务字段,但是经常使用,可能编写一个TO(Transfer Object)来传输数据
Page{
int index;
int size;
}
简单参数
Employee getEmployeeByIdAndName(@Param("id") int id, String lastName);
取id的值: #{id}或#{param1}
取lastName的值:#{param2}
对象参数
Employee getEmployeeByIdAndEmp(@Param("id") int id, @Param("e")Employee emp);
取id的值: #{id}或#{param1}
取lastName的值:#{e.lastName}或者#{param2.lastName} --param2代表emp对象
集合参数
如果是Collection类型或者数组类型,会特殊处理,将Collection类型或者数组类型封装在map中
Collection类型: key为collection, 如果是list会进一步封装key为list
数组类型: key为array
Employee getEmployeeByIds(List<int> ids);
取第一个id的值:#{list[0]}
$和#都可以用来获取参数
#: 是以预编译的方式将参数设置到SQL中去,可以防止SQL注入,大多数情况下使用#取值
$: 将取出的值直接拼装在SQL中,有SQL注入的安全风险
$ 可以用来动态设置SQL中的表名和字段名称,
原生SQL不支持占位符的地方都可以用$来处理
/**
* 测试 testDynamicSql
*/
@Test
public void testDynamicSql() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
//map参数
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("p_gender", "F");
paramMap.put("tableName", "Employee");
paramMap.put("sortFieldName", "last_name");
paramMap.put("sort", "ASC");
List<Employee> employees = employeeMapper.dynamicSql(paramMap);
for (Employee employee : employees) {
System.out.println(employee.toString());
}
openSession.commit();
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
<!--动态SQL-->
<select id="dynamicSql" resultType="com.mybatis.entity.Employee">
select *
from ${tableName}
where gender = #{p_gender}
order by ${sortFieldName} ${sort}
</select>
#{}可以规定参数的类型
javaType, jdbcType, resultMap, typeHandler, jdbcTypeName
jdbcType: 在数据为null的情况下,数据库不能识别mybatis对于null的处理比如Oracle
<!--
MyBatis将所有的null都映射为原生的 OTHER(Types.OTHER)
Oracle无法处理Other类型
需要指定字段值为空时的JDBC类型jdbcType=NULL
或者在全局配置中设置 <setting name="jdbcTypeForNull" value="NULL"/>
-->
<insert id="addEmployeeWithNull" parameterType="com.mybatis.entity.Employee" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
select EMPLOYEES_SEQ.nextval from dual
</selectKey>
<!-- 插入时的主键是从序列中拿到 -->
insert into employee (id, last_name, email, gender)
values (#{id}, #{lastName}, #{email, jdbcType=NULL}, #{gender})
</insert>
<if test="@cn.hutool.core.util.StrUtil@isNotEmpty(clientAdminBean.name) ">
and a.name = #{clientAdminBean.name}
</if>
<if test="@cn.hutool.core.util.StrUtil@isNotEmpty(clientAdminBean.tableName) ">
and a.table_name like CONCAT('%',#{clientAdminBean.tableName},'%')
</if>
<if test="@cn.hutool.core.util.StrUtil@isNotEmpty(clientAdminBean.mqTopic) ">
and a.mq_topic like CONCAT('%',#{clientAdminBean.mqTopic},'%')
</if>
resultType为集合中元素的类型
resultType为的类型为map
<!--测试返回结果为map-->
<select id="getEmployeeReturnMap" resultType="map">
select *
from employee t
where t.id = #{id}
</select>
/**
* 测试 testMapResult
*/
@Test
public void testMapResult() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Map<String, Object> employeeMap = employeeMapper.getEmployeeReturnMap(4);
openSession.commit();
for (Map.Entry<String, Object> entry : employeeMap.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
接口类
/**
* 多条记录封装在Map<String, Employee>
* MapKey 告诉mybatis用Employee的哪个属性作为map的key
*/
@MapKey("id")
Map<Integer, Employee> getEmployeeByGenderReturnMap(String gender);
SQL映射文件
<!--测试返回结果为多个MAP对象-->
<select id="getEmployeeByGenderReturnMap" resultType="com.mybatis.entity.Employee">
select *
from employee t
where t.gender = #{gender}
</select>
/**
* 测试返回为多个对象
*/
@Test
public void testMapResult2() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
//返回为多个对象
Map<Integer, Employee> employeeMap = employeeMapper.getEmployeeByGenderReturnMap("F");
openSession.commit();
for (Map.Entry<Integer, Employee> entry : employeeMap.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
resultType 是自动封装,将表中的字段与JavaBean中的字段映射
resultMap 是自定义封装,实现高级结果集映射
定义ResultMap
<!--
自定义表字段名称与JavaBean属性的映射
type: JavaBean类型
id: resultMap唯一标识,方便被引用
-->
<resultMap id="employeeMap" type="com.mybatis.entity.Employee">
<!--
column: 表字段名称
property: JavaBean类型属性
-->
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!--不在此处配置的列,会自动映射,但如果使用resultMap,建议把所有列都配置出来-->
</resultMap>
<select id="getEmployeeByGenderReturnMap" resultMap="employeeMap">
select *
from employee t
where t.gender = #{gender}
</select>
业务类
/**
* 多条记录封装在Map<String, Employee>
* MapKey 告诉mybatis用Employee的哪个属性作为map的key
*/
@MapKey("id")
Map<Integer, Employee> getEmployeeByGenderReturnMap(String gender);
测试类
/**
* 测试resultMap
*/
@Test
public void testResultMap() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper2 employeeMapper2 = openSession.getMapper(EmployeeMapper2.class);
Map<Integer, Employee> employeeMap = employeeMapper2.getEmployeeByGenderReturnMap("F");
openSession.commit();
for (Map.Entry<Integer, Employee> entry : employeeMap.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
定义ResultMap
<!--通过级联属性的方式进行联合查询-->
<resultMap id="empDeptMap" type="com.mybatis.entity.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<result column="did" property="dept.id"/>
<result column="department_name" property="dept.name"/>
</resultMap>
<select id="getEmpAndDept" resultMap="empDeptMap">
select t.id, t.last_name, t.email, t.gender, d.id did, d.department_name
from employee t,
department d
where t.id = #{id}
and t.department_id = d.id
</select>
业务类
@Data
@AllArgsConstructor
public class Employee {
public Employee(String lastName, String email, String gender) {
this.lastName = lastName;
this.email = email;
this.gender = gender;
}
public Employee(int id, String lastName, String email, String gender) {
this(lastName, email, gender);
this.id = id;
}
private int id;
private String lastName;
private String email;
private String gender;
private Department dept;
}
测试类
/**
* 通过级联属性的方式进行联合查询
*/
@Test
public void testAssociateResultMap() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper2 employeeMapper2 = openSession.getMapper(EmployeeMapper2.class);
Employee employee = employeeMapper2.getEmpAndDept(1);
openSession.commit();
System.out.println(employee.toString());
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
SQL映射文件
DepartmentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.dao.mapper.DepartmentMapper">
<select id="getDeptById" resultType="com.mybatis.entity.Department">
select * from department t where t.id=#{id}
</select>
</mapper>
EmployeeMapper2.xml
<!--使用association进行分步查询-->
<resultMap id="empDeptBySteps" type="com.mybatis.entity.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!--
1.先根据员工id查询到员工信息
2.再根据员工信息中的d_id查询部门信息
3.再将部门信息封闭到员工对象中
select:表示当前关联的对象是调用指定的方式查询出来的结果
column="department_id":指定传入select中方法的参数,此参数来自于第一个方法“getEmpById”中查询的结果集里面
-->
<association property="dept" javaType="com.mybatis.entity.Department"
select="com.mybatis.dao.mapper.DepartmentMapper.getDeptById" column="department_id"/>
</resultMap>
<select id="getEmpById" resultMap="empDeptBySteps">
select * from employee t where t.id = #{id}
</select>
业务类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private int id;
private String name;
}
package com.mybatis.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private int id;
private String lastName;
private String email;
private String gender;
private Department dept;
public Employee(String lastName, String email, String gender) {
this.lastName = lastName;
this.email = email;
this.gender = gender;
}
public Employee(int id, String lastName, String email, String gender) {
this(lastName, email, gender);
this.id = id;
}
}
测试类
/**
* 使用association进行分步查询
*/
@Test
public void testEmpDeptBySteps() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
EmployeeMapper2 employeeMapper2 = openSession.getMapper(EmployeeMapper2.class);
Employee employee = employeeMapper2.getEmpById(1);
openSession.commit();
System.out.println(employee.toString());
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
全局配置文件
<!--开启全局延时加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--关闭积极加载-->
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value=""/>
业务类
一般业务类中使用@Data注解会触发toString方法,可以将@Data替换成@Getter和@Setter注解
package com.mybatis.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private int id;
private String lastName;
private String email;
private String gender;
private Department dept;
public Employee(String lastName, String email, String gender) {
this.lastName = lastName;
this.email = email;
this.gender = gender;
}
public Employee(int id, String lastName, String email, String gender) {
this(lastName, email, gender);
this.id = id;
}
}
SQL映射文件
<!--嵌套结果集关联查询-->
<resultMap id="deptWithEmps" type="com.mybatis.entity.Department">
<id column="did" property="id"/>
<result column="name" property="name"/>
<!--
定义集合类型的属性
ofType:指定集合里面的元素类型
-->
<collection property="emps" ofType="com.mybatis.entity.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</collection>
</resultMap>
<select id="getDeptById2" resultMap="deptWithEmps">
SELECT d.id did, d.department_name, e.id, e.email, e.gender, e.last_name, e.department_id
FROM department d
LEFT JOIN employee e ON e.department_id = d.id
WHERE d.id = #{id}
</select>
业务类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private int id;
private String name;
private List<Employee> emps;
}
测试类
/**
* 使用collection进行关联对象的集合查询
*/
@Test
public void testDeptWithEmps() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper = openSession.getMapper(DepartmentMapper.class);
Department dept = departmentMapper.getDeptById2(1);
openSession.commit();
System.out.println(dept.toString());
System.out.println(dept.getEmps().toString());
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
SQL映射文件
DepartmentMapper.xml
<!--
分步查询部门下的员工信息
1.先查询出部门信息
2.再根据部门ID查询出员工信息
-->
<resultMap id="deptWithEmpsByStep" type="com.mybatis.entity.Department">
<id column="id" property="id"/>
<result column="department_name" property="name"/>
<!--
column="id":指定传入select中方法的参数,此参数来自于第一个方法“getDeptByIdStep”中查询的结果集里面
多个参数时column={key1=value1,key2=value2} key要和也分步查询方法里面的参数名称保持一致
javaType:返回结果集的类型
fetchType: 即使全局打开了延时加载,也可以指定针对此关联查询是否要进行延时加载,
eager:立即加载,lazy:延时加载
-->
<collection property="emps" select="com.mybatis.dao.mapper.EmployeeMapper.getEmployeeByDeptId" column="id"
javaType="List" fetchType="lazy"/>
</resultMap>
<select id="getDeptByIdStep" resultMap="deptWithEmpsByStep">
select * from department t where t.id=#{id}
</select>
EmployeeMapper.xml
<!--
分步查询部门下的员工信息
1.先查询出部门信息
2.再根据部门ID查询出员工信息
-->
<select id="getEmployeeByDeptId" resultType="com.mybatis.entity.Employee">
select *
from employee
where department_id = #{department_id}
</select>
业务类
package com.mybatis.dao.mapper;
import com.mybatis.entity.Employee;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface EmployeeMapper {
Employee getEmployeeByDeptId(int i);
}
测试类
/**
* 使用collection进行关联对象的集合分步查询
*/
@Test
public void testDeptWithEmpsBySteps() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper = openSession.getMapper(DepartmentMapper.class);
Department dept = departmentMapper.getDeptByIdStep(2);
openSession.commit();
System.out.println(dept.toString());
System.out.println(dept.getEmps().toString());
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
SQL映射文件
EmployeeMapper2
<!--
discriminator
鉴别器
mybatis可以根据discriminator判断某列的值,根据值来改变封装行为
封装employee
1.如果查询出来的是女生,则把对应的部门信息查询出来,否则不查询
2.如果查询出来的是男生,则把lastName这一列赋值到email上
-->
<resultMap id="empDeptByStepsWithDiscriminator" type="com.mybatis.entity.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<!--如果查询出来的是女生,则把对应的部门信息查询出来,否则不查询-->
<discriminator javaType="String" column="gender">
<case value="F" resultType="com.mybatis.entity.Employee">
<association property="dept" select="com.mybatis.dao.mapper.DepartmentMapper.getDeptById"
javaType="com.mybatis.entity.Employee"
column="department_id" fetchType="eager"/>
</case>
<!--如果查询出来的是男生,则把lastName这一列赋值到email上-->
<case value="M" resultType="com.mybatis.entity.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="last_name" property="email"/>
<result column="gender" property="gender"/>
</case>
</discriminator>
</resultMap>
<select id="getEmpByIdwithDiscriminator" resultMap="empDeptByStepsWithDiscriminator">
select * from employee
</select>
DepartmentMapper.xml
<select id="getDeptById" resultType="com.mybatis.entity.Department">
select id, department_name name from department t where t.id=#{id}
</select>
业务类
public interface EmployeeMapper2 {
List<Employee> getEmpByIdwithDiscriminator();
}
测试类
@Test
public void testEmpByUsingDiscriminator() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession(true);
EmployeeMapper2 employeeMapper2 = openSession.getMapper(EmployeeMapper2.class);
List<Employee> emps = employeeMapper2.getEmpByIdwithDiscriminator();
for (Employee emp : emps) {
System.out.println(emp.toString());
if (null != emp.getDept()) {
System.out.println(emp.getDept().toString());
}
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
结果
Employee{id=1, lastName='Chris', email='Chris', gender='M'}
Employee{id=3, lastName='Hedy', email='Hedy', gender='M'}
Employee{id=4, lastName='Nancy', email='Nancy@gmail.com', gender='F'}
Department(id=1, name=开发部, emps=null)
Employee{id=7, lastName='Cano', email='Cano@gmail.com', gender='F'}
Department(id=2, name=测试部, emps=null)
<select id="getempsbyconditionIf" resultType="com.mybatis.entity.Employee">
select * from employee t
<!--
where:用来封装查询条件,会将SQL中多出来的and和or去掉,但是只去掉第一个and和or
-->
<where>
/* 1. test:判断表达式,使用OGNL表达式
https://commons.apache.org/proper/commons-ognl/language-guide.html
遇见特殊符号应该转为转义字符*/
<if test="id != null">
t.id = #{id}
</if>
<if test="lastName != null and lastName!= '' ">
and t.last_name=#{lastName}
</if>
<if test="email != null and email.trim() !='' ">
and t.email=#{email}
</if>
<if test='gender != null and (gender =="F" || gender =="M")'>
and t.gender=#{gender}
</if>
</where>
</select>
<select id="getempsbyconditionTrim" resultType="com.mybatis.entity.Employee">
select * from employee t
<!--
prefix: 对trim中整个字符串拼串后的结果加一个前缀
prefixOverrides: 前缀覆盖,去掉整个拼串前面多余的字符串
suffix: 对trim中整个字符串拼串后的结果加一个后缀
suffixOverrides: 后缀覆盖,去掉整个拼串前面多余的字符串
-->
<trim prefix="where" suffixOverrides="and">
<if test="id != null">
t.id = #{id} and
</if>
<if test="lastName != null and lastName!= '' ">
t.last_name=#{lastName} and
</if>
<if test="email != null and email.trim() !='' ">
t.email=#{email} and
</if>
<if test='gender != null and (gender =="F" || gender =="M")'>
t.gender=#{gender}
</if>
</trim>
</select>
<select id="getempsbyconditionChoose" resultType="com.mybatis.entity.Employee">
select * from employee t
<where>
<choose>
<when test="id!=null">id=#{id}</when>
<when test="lastName!=null">last_name=#{lastName}</when>
<when test="email!=null and email.trim()!='' ">email=#{email}</when>
<otherwise>
<!--查询所有-->
1 = 1
</otherwise>
</choose>
</where>
</select>
<select id="getRolePrincileByFormIdandRoleKey" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM
t_wf_role_principle p
WHERE
p.formId = #{formId}
<choose>
<when test="null != roleKeys and roleKeys.size > 1">
<foreach collection="roleKeys" item="key" open=" and p.roleKey in (" close=")"
separator=",">
#{key}
</foreach>
</when>
<when test="null != roleKeys and roleKeys.size == 1">
and p.roleKey = #{roleKeys[0]}
</when>
</choose>
</select>
<!--
参数中带了哪一个参数则更新此参数对应的字段
<set>用来封装修改字段
也可以用tirm替换set标签
<trim prefix="set" prefixOverrides=",">
-->
<update id="updateEmp">
update employee t
<set>
<if test="lastName!=null and lastName.trim()!=''">
t.last_name=#{lastName},
</if>
<if test="email!=null and email.trim()!=''">
t.email=#{email},
</if>
<if test='gender!=null and (gender =="F" || gender =="M")'>
t.gender=#{gender},
</if>
</set>
where t.id = #{id}
</update>
<!--
connection:指定要遍历的集合名称
list类型的参数会特殊处理封装在map中,这个map的key名称是list
item:将当前遍历出的值赋值给指定变更
separator元素之间的分隔符
open:封装遍历结果的开始字符
close:封装遍历结果的结束字符
index:索引,遍历list时,index是当前元素的索引,item为当前元素的值
遍历map时,index是当前元素的key,item为当前元素的值
#{变量名}就能取出变量的值也就是当前遍历出的元素
-->
<select id="getEmpsByConditionForeach" resultType="com.mybatis.entity.Employee">
select * from employee where id in
<foreach collection="list" item="id" separator="," open="(" close=")" index="inx">
#{id}
</foreach>
</select>
public interface DynamicSqlMapper {
List<Employee> getEmpsByConditionForeach(List<Integer> ids);
}
/**
* 测试foreach
*/
@Test
public void testForeach() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession(true);
DynamicSqlMapper dynamicSqlMapper = openSession.getMapper(DynamicSqlMapper.class);
List<Employee> employees = dynamicSqlMapper.getEmpsByConditionForeach(Arrays.asList(1, 3, 4));
openSession.commit();
for (Employee employee : employees) {
System.out.println(employee.toString());
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
方式一
SQL映射文件
<insert id="addEmpsInBatch">
insert into employee (last_name, email,gender,department_id)
<foreach collection="emps" item="emp" open="values" separator=",">
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>
业务类
int addEmpsInBatch(@Param("emps") List<Employee> employees);
测试类
/**
* 测试foreach insert
*/
@Test
public void testForeachInsert() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession(true);
DynamicSqlMapper dynamicSqlMapper = openSession.getMapper(DynamicSqlMapper.class);
Employee emp1 = new Employee("S4", "s4@gmail.com", "F", new Department(1));
Employee emp2 = new Employee("S5", "s5@gmail.com", "M", new Department(2));
Employee emp3 = new Employee("S6", "s6@gmail.com", "F", new Department(2));
int count = dynamicSqlMapper.addEmpsInBatch(Arrays.asList(emp1, emp2, emp3));
System.out.println("new added employee count:" + count);
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
方式二
SQL映射文件
<!-- Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'insert into employee (last_name, email,gender,department_id)values
(' at line 4
需要打开MYSQL的批量写入模式:allowMultiQueries=true
jdbc.home.url=jdbc:mysql://192.168.101.127:3306/chris?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
-->
<insert id="addEmpsInBatch2">
<foreach collection="emps" item="emp" separator=";">
insert into employee (last_name, email,gender,department_id)values
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>
SQL中映射文件
<!--
oracle 不支持mysql这种批量写入数据库
insert into employee(last_name, email,gender,department_id) values (),()
但是支持如下三种
1. 将多条语句放在begin和end之间
begin
insert into employee(last_name, email,gender,department_id) values ();
insert into employee(last_name, email,gender,department_id) values ();
insert into employee(last_name, email,gender,department_id) values ();
end;
2. 利用中间表
insert into employee(last_name, email,gender,department_id) select employees_seq.nextval, last_name, email from (
select 'test_a_01' last_name, 'test_a_01@163.com' email from dual;
union
select 'test_a_02' last_name, 'test_a_02@gmail.com' email from dual;
union
select 'test_a_03' last_name, 'test_a_03@gmail.com' email from dual;
)
-->
<!--1. 将多条语句放在begin和end之间-->
<insert id="addEmpsInBatchInOracle" databaseId="oracle">
<foreach collection="emps" item="emp" open="begin" close="end;" separator=";">
insert into employee (last_name, email,gender,department_id)
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>
<!--2. 利用中间表-->
<insert id="addEmpsInBatch2InOracle" databaseId="oracle">
insert into employee(last_name, email,gender,department_id)
<foreach collection="emps" item="emp"
open="select employees_seq.nextval, last_name, email, gender, department_id from(" close=")"
separator="union">
select #{emp.lastName} last_name, #{emp.email} email, #{emp.gender} gender, #{emp.dept.id} department_id
from dual;
</foreach>
</insert>
<!--
两个内置参数
1. _parameter: 代表传入方法的整个参数
如果是一个参数,_parameter就表示这个参数
如果是多个参数,参数会被mybatis封装成为一个map,_parameter就代表这个map
2. _databaseId: 如果配置了databaseIdProvider,_databaseIdy就是代表当前使用的数据库别名
-->
<!--根据当启用的数据库来进行不同的查询-->
<select resultType="com.mybatis.entity.Employee">
<if test="_databaseId=='mysql'">
select * from employee
<where>
<if test="_parameter!=null">
last_name = #{_parameter.lastName}
</if>
</where>
</if>
<if test="_databaseId=='mysql'">
select * from t_employee
</if>
</select>
SQL映射文件
<!--如果lastName有值则进行模糊查询-->
<select id="getEmpByBind" resultType="com.mybatis.entity.Employee">
<!--bind 将OGNL表达式的值绑定到一个变量中,方便后面引用这个变量的值-->
<bind name="_lastName" value="'%'+lastName+'%'"/>
select * from employee
<where>
<if test="_parameter != null and lastName !='' ">
last_name LIKE #{_lastName}
</if>
</where>
</select>
业务类
public interface DynamicSqlMapper {
List<Employee> getEmpByBind(Employee emp);
}
测试类
/**
* 测试bind
*/
@Test
public void testBind() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession(true);
DynamicSqlMapper dynamicSqlMapper = openSession.getMapper(DynamicSqlMapper.class);
Employee emp = new Employee();
// 查询名称中包括a的员工记录
emp.setLastName("a");
List<Employee> emps = dynamicSqlMapper.getEmpByBind(emp);
for (Employee employee : emps) {
System.out.println(employee.toString());
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
SQL映射文件
<!--
include
1.可以引用sql标签中定义的SQL片断
2.可以定义属性变量,可以在sql标签使用${testColumn}来获取属性变量的值
-->
<insert id="addEmpsWithCondistionInclude">
insert into employee (
<include refid="emp_insert_fields">
<property name="testColumn" value="abc"/>
</include>
)
<foreach collection="emps" item="emp" open="values" separator=",">
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id})
</foreach>
</insert>
<!--
sql标签用来抽取可重用的sql片段
1.将常用的列名抽取出来,方便查询或插入中引用
2.include来引用sql标签中的sql片断
-->
<sql id="emp_insert_fields">
last_name, email,gender,department_id,${testColumn}
</sql>
mybatis中默认定义了两级缓存
默认情况有只开启一级缓存,也称为本地缓存,即SqlSesssion级别的缓存
二级缓存为全局缓存,需要手动配置和开启,它是基于namespace级别的缓存
为了提高扩展性,mybatis提供了缓存接口Cache,可以通过实现Cache接口来实现二级缓存
与数据库同一次会话期间查询到的数据会放在一级缓存中,以后如果需要获取相同的数据,直接从一级缓存中查
SQL映射
<select id="getEmpById" resultType="com.mybatis.entity.Employee">
select * from employee where id = #{id}
</select>
测试类
/**
* 测试Cache
*/
@Test
public void testCache() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession = sqlSessionFactory.openSession(true);
CacheMapper cacheMapper = openSession.getMapper(CacheMapper.class);
// 第一次查询
Employee emp = cacheMapper.getEmpById(1);
System.out.println(emp.toString());
// 第二次查询
Employee emp2 = cacheMapper.getEmpById(1);
System.out.println(emp2.toString());
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (null != openSession) {
openSession.close();
}
}
}
测试结果
DEBUG - ==> Preparing: select * from employee where id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
SqlSession不是同一个,则一级缓存失效
SqlSession相同,但是需要查询的数据在一级缓存中没有,则不走缓存,需要去数据库中查询
SqlSession相同,但是两次查询中间有增删改的SQL操作,因为mybatis认为增删改可能对之后的查询有影响
SqlSession相同,但是手动清除了一级缓存
openSession.clearCache();
一个会话查询一条数据,这条数据会被放在当前会话的一级缓存中
如果当前会话关闭,此会话对应的一级缓存中的数据会被保存到二级缓存中,新的会话查询信息时,就会参照二级缓存
如果sqlsession即有employee信息又有department信息,二级缓存会根据不同对象信息的namespace缓存数据
EmployeeMapper -> employee
DeapartmentMapper-> department
全局配置文件中开启二级缓存
<!--全局开启二级缓存,默认已开启-->
<setting name="cacheEnabled" value="true"/>
需要使用二级缓存的映射文件处使用cache配置缓存
<cache />
POJO需要实现序列化接口
public class Department implements Serializable
public class Employee implements Serializable
测试类
查出的数据都会先放在一级缓存中
只有会话被关闭,一级缓存中的数据才会转移到二级缓存中,其它SqlSesssion才能在二级缓存中获取此数据
/**
* 测试二级缓存
*/
@Test
public void testSecondCache() throws IOException {
SqlSessionFactory sqlSessionFactory;
SqlSession openSession_1 = null;
SqlSession openSession_2 = null;
try {
sqlSessionFactory = getSqlSessionFactory();
openSession_1 = sqlSessionFactory.openSession(true);
CacheMapper cacheMapper1 = openSession_1.getMapper(CacheMapper.class);
openSession_2 = sqlSessionFactory.openSession(true);
CacheMapper cacheMapper2 = openSession_2.getMapper(CacheMapper.class);
// 第一次查询
Employee emp = cacheMapper1.getEmpById(1);
System.out.println(emp.toString());
// 查出的数据都会先放在一级缓存中,只有会话被关闭,一级缓存中的数据才会转移到二级缓存中,其它opensesssion才能在二级缓存中获取
closeSession(openSession_1);
// 第二次查询
Employee emp2 = cacheMapper2.getEmpById(1);
System.out.println(emp2.toString());
closeSession(openSession_2);
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
closeSession(openSession_1, openSession_2);
}
}
结果
DEBUG - Cache Hit Ratio [com.mybatis.dao.mapper.CacheMapper]: 0.5
DEBUG - ==> Preparing: select * from employee where id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
DEBUG - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@27c6e487]
DEBUG - Returned connection 667346055 to pool.
DEBUG - Cache Hit Ratio [com.mybatis.dao.mapper.CacheMapper]: 0.5
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
全局开启二级缓存,默认已开启 ,
false:仅关闭二级缓存,但是一级缓存仍然可用
<setting name="cacheEnabled" value="true"/>
每一个select标签都有一个useCache="true"
ture: 表示使用二级缓存,但是必须将全局二级缓存打开
false:表示不使用二级缓存,但一级缓存仍然可用
<select id="getEmpById" resultType="com.mybatis.entity.Employee" useCache="true">
select * from employee where id = #{id}
</select>
每一个查询标签都有一个flushCache="fase",如何改为flushCache="true"表示查询执行完之后都会清除一级和二级缓存
每一个增删改标签都有一个flushCache="true",表示增删改执行完之后都会清除一级和二级缓存
测试
虽然去缓存中找数据,但是数据已经被清除
DEBUG - ==> Preparing: select * from employee where id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
DEBUG - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@27c6e487]
DEBUG - Returned connection 667346055 to pool.
DEBUG - Opening JDBC Connection
DEBUG - Checked out connection 667346055 from pool.
DEBUG - ==> Preparing: insert into employee ( last_name, email, gender, department_id ) values (?,?,?,?)
DEBUG - ==> Parameters: shawn(String), shawn@qq.com(String), M(String), 1(Integer)
DEBUG - <== Updates: 1
DEBUG - Cache Hit Ratio [com.mybatis.dao.mapper.CacheMapper]: 0.5
DEBUG - ==> Preparing: select * from employee where id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
DEBUG - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@27c6e487]
DEBUG - Returned connection 667346055 to pool.
flushCache="false"
增删改后,查询时会使用二级缓存
DEBUG - ==> Preparing: select * from employee where id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
DEBUG - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@27c6e487]
DEBUG - Returned connection 667346055 to pool.
DEBUG - Opening JDBC Connection
DEBUG - Checked out connection 667346055 from pool.
DEBUG - ==> Preparing: insert into employee ( last_name, email, gender, department_id ) values (?,?,?,?)
DEBUG - ==> Parameters: shawn(String), shawn@qq.com(String), M(String), 1(Integer)
DEBUG - <== Updates: 1
DEBUG - Cache Hit Ratio [com.mybatis.dao.mapper.CacheMapper]: 0.5
Employee{id=1, lastName='Chris', email='chris@gmail.com', gender='M'}
DEBUG - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@27c6e487]
DEBUG - Returned connection 667346055 to pool.
只清空一级缓存,不会清空二级缓存,因为clearCache方法属于sqlsession级的方式,不是namesapce级别的方式
<!--
本地缓存作用域,默认 SESSION
SESSION | STATEMENT
1.SESSION:当前会话sqlsession的所有查询数据都会被缓存
2.STATEMENT:同一会话的两次不同的sql间不会共享数据发,即可以禁用一级缓存
-->
<setting name="localCacheScope" value="SESSION"/>

https://github.com/mybatis/ehcache-cache
http://mybatis.org/ehcache-cache/
改pom
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>${mybatis.ehcache.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
建ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--
属性说明:
diskStore:指定数据在磁盘中的存储位置。
defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
maxElementsInMemory - 在内存中缓存的element的最大数目
maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
<!-- 磁盘保存路径 -->
<diskStore path="D:\\ehcache"/>
<defaultCache
maxElementsInMemory="10000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
在SQL映射文件中引入ehcache实现类
<!--
type:自定义缓存的全类名,自定义缓存需要实现Cache接口
-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
https://github.com/mybatis/spring
http://mybatis.org/spring/

| 创建时间: | 2022/12/15 22:24 |
| 更新时间: | 2023/1/4 17:37 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/SeekN/article/details/114231727 |
双端队列(Double Ended Queue),学名Deque
Java集合提供了接口Deque来实现一个双端队列,它的功能是:既可以添加到队尾,也可以添加到队首; 既可以从队首获取,又可以从队尾获取。

add()和offer()都是向队列中添加一个元素。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,调用 add() 方法就会抛出一个 unchecked 异常,而调用 offer() 方法会返回 false。因此就可以在程序中进行有效的判断!
remove() 和 poll() 方法都是从队列中删除第一个元素。如果队列元素为空,调用remove() 的行为与 Collection 接口的版本相似会抛出异常,但是新的 poll() 方法在用空集合调用时只是返回 null。因此新的方法更适合容易出现异常条件的情况。
element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。
Queue的数据结构是一个队列,即:FIFO(先进先出)。从队尾添加元素,从对头删除元素。Deque也有等效的方法作为一个FIFO队列,具体方法如下:

Deque(双端队列)也可以用作LIFO(后进先出)堆栈(也就是栈)。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于Deque 方法,如下表所示:

LinkedList 本质是双向链表,是可以当做队列或栈使用的。因为他有push pop(栈操作方法)addFirst、addLast、removeFirst、removeLast(队列操作方法)
add 与 offer 将 LinkedList 当作链表或队列来使用。而 push 操作是将 LinkedList 当作栈来使用。add(不带索引默认添加到链表的最后)与 offer一样都是添加操作,唯一的区别就是 offer 没有带索引参数的方法,并且如果队列满了 > add 会抛出异常,而 offer 不会。
LinkedList 的取出操作只有 pop 和 get。并且如果一个 LinkedList 中既有 add 或 offer 的添加,又有 push 的添加,那么 pop 操作会先取栈元素,再取队列元素。
ArrayDeque 是 Deque 的一个实现。ArrayDeque 俗称数组双端队列,是一种允许我们从俩端进行存取操作的可扩容数组。ArrayDeque 的底层是由一个数组来实现的,这个数组会在其塞满的时候,把容量扩大一倍。该数组初始化大小为16,它通过维护俩个指针:head、tail 实现了双端队列。
当用户使用 push 方法向其中添加元素时,它会把 head 头指针向前移动一位。当从栈中弹出一个元素时,它会把 head 位置处的元素设置为 null (这样的话,此元素就可以被垃圾回收),并且把 head 指针向后移动一位。
| 创建时间: | 2022/12/17 23:26 |
| 更新时间: | 2022/12/18 21:17 |
| 作者: | Chris |
MongoDB 官网地址:https://www.mongodb.com/
MongoDB 官方英文文档:https://www.mongodb.com/docs/v5.0/
https://www.mongodb.com/docs/v5.0/reference/operator/query/#std-label-query-selectors
MongoDB是由C++编写的基于分布式文件存储的数据库,旨在为web应用提供可扩展高性能的数据存储解决方案。
关系型数据库和非关系型数据是相辅相成了
关系型数据库的强事务特性在非关系型数据是没有的,所以一些关键的核心业务数据还是要存储在关系型数据库中,比如用户的订单,帐户金额等
对于一些查询写入速度要求高但对事务要求不高的数据可以放到非关系型数据中去存储
2009年2月,MongoDB首次在数据库领域亮相,打破了关系性数据库一统天下的局面
2010年8月,MongoDB1.6版本发布,最大的一个功能就是支持Sharding自动分片
2014年12月,MongoDB 3.0版本发布,由于收购了WiredTiger存储引擎,大幅提升了MongoDB写入性能
2015年12月,MongoDB3.2版本发布,开始支持关联查询,查以一次性查询多个MongoDB集合
2016年,MongoDB推出Atlas, 在AWS, Azure和GCP上的MongoDB托管服务
2017年10月,MongoDB成功在纳斯达克上市
2018年6月, MongoDB4.0版本发布推出ACID事务支持,成为第一个支持强事务的NOSQL数据库
http://mongodb.com/try/download/community

bin目录用来存放启动和关闭的脚本

install_compass 是来用安装compass工具
mongo是用来启动mongodb客户端
mongod是用来启动mongo服务的启动脚本
mongos路由脚本
mkdir ../datas ../logs
./mongod --port=27017 --dbpath=../data --logpath=../logs/mongo.log
--port 指定服务监听商品号默认27017
--dbpath 指定mongodb数据存放目录,启动时目录必须存在
--logpath 指定mongodb日志文件存放位置
./mongod --help
搜索mongo
进入 mongo



[root@master ~]# docker search mongo
[root@master ~]# docker pull mongo:5.0.5
[root@master ~]# docker run -d --name="mongo" -p 27017:27017 mongo:5.0.5
[root@master ~]# docker exec -it mongo /bin/bash
root@a4634a4f15c7:/# mongo ##启动mongdodb
> show dbs;
admin 0.000GB
config 0.000GB
local 0.000GB
> exit ##只是退出当前mongodb的客户端
bye
root@a4634a4f15c7:/#
mongodb中的库的概念类似于传统关系型数据库中库的概念,用来隔离不同应用的数据
mongodb中可以创建多个库,每一个库都有自己的集合和权限,不同的数据库放置在不同的文件中
默认数据库为test,数据存储在启动时指定的目录中。
集合就是MongoDB中文档组,类似于关系型数据库中表的概念
集合存在于数据库中,一个库可以有多个集合,每个集合没有固定的结构,这意味着可以对集合插入不同格式和类型的数据,但通常情况下插入集合的数据都会有一定的关联性
文档是集合中的一条条记录,是一组键值key-value对即Bson.
MongoDB的文档不需要设置相同的字段,并且相同的字段不需要相同的类型,这与关系型数据库有很大的区分,也是MongoDB非常突出的特点。
一个简单的文档如下:
{"site":"www.baidu.com", "name":"chris"}

show dbs; | show databases;
db 查询当前所在的库
> show dbs;
admin 0.000GB
config 0.000GB
local 0.000GB
> db
test
> use chris
switched to db chris
> show dbs;
admin 0.000GB
config 0.000GB
local 0.000GB
> db.users.insert({'name':'chris'})
WriteResult({ "nInserted" : 1 })
> show dbs;
admin 0.000GB
chris 0.000GB
config 0.000GB
local 0.000GB
admin: 从权限的角度来看,这人是root 数据库,要是将一个用户添加到这个数据库,这个用户自动继承所有的数据库权限,一些特殊的服务器端命令也只能从这个数据库运行,比如列出所有数据库或者关闭服务器。
local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
config: 当MongoDB分片设置时,config数据库在内部使用,用于保存分片相关的信息
当打开客户端时默认连接是test库
root@a4634a4f15c7:/# mongo
MongoDB shell version v5.0.5
当库存在时切换库,库不存在时创建并使用库,当库里面没有数据默念不显示这个库
use 数据库名称
删除当前库
db.dropDatabase();
> db
chris
> db.dropDatabase()
{ "ok" : 1 }
> db
chris
查看库中的集合
show collections | show tables
> show collections;
users
> show tables;
users
创建集合
db.createCollection('集合名称',[options])
> db.createCollection('users');
{
"ok" : 0,
"errmsg" : "Collection already exists. NS: chris.users",
"code" : 48,
"codeName" : "NamespaceExists"
}
> db.createCollection('products');
{ "ok" : 1 }
options:
| 字段 | 类型 | 描述 |
|---|---|---|
| capped | 布尔 | 如果为ture必须指定size参数,则创建固定集合,固定集合是指有固定大小的集合,当达到最大值时,会自动覆盖掉最最早的文档。 |
| size | 数值 | 为固定集合指定一个最大值,即字节数 |
| max | 数值 | 指定固定集合中包含文档的最大数量 |
当集合不存在时,向集合中插入文档也会自动创建集合
删除集合
db.集合名称.drop();
db.orders.drop();
https://docs.mongodb.com/manual/reference/method
https://www.mongodb.com/docs/v5.0/crud/
db.集合名称.insert({name:'chris',age:12})
db.集合名称.insertMany(
[<doc1>,<doc2>],
{
writeConcern:1,//写入策略,默认为1即要求确认写操作,0是不要求
ordered:true //是否按顺序写入,默认是true
}
db.users.insertMany([
{name:'John', age:23},
{name:'Rebecca', age:64}
]);
db.users.insert([
{name:'Wonderful', age:32, birthday:'2010-12-31 21:23:21'},
{name:'Dave', age:22, birthday:'2010-03-12 11:27:11'}
]);
-- 返回结果
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
for(let i=0; i<10; i++){
db.users.insert({_id:i,name:'Chris_'+i,age:32+i});
}
在mongodb中每个文档都会有一个
_id作为文档的唯一标识,_id默认会自动生成,如果手动指定将使用手动指定的_id值。
查询所有
db.集合名称.find();
条件查询
db.集合名称.find(query, projection).pretty();
query:
可选,指定的查询条件projection:
可选pretty():对超过一定长度的文档进行格式化显示
| 操作 | 格式 | 例子 | 类比SQL |
|---|---|---|---|
| 等于 | {key:value} | db.users.find({name:'chris'}).pretty(); | where name='chris' |
| 小于 | {key:{$lt:value}} | db.users.find({age:{$lt:23}}).pretty(); | where age<23 |
| 小于等于 | {key:{$lte:value}} | db.users.find({age:{$lte:23}}).pretty(); | where age<=23 |
| 大于 | {key:{$gt:value}} | db.users.find({age:{$gt:23}}).pretty(); | where age>23 |
| 大于等于 | {key:{$gte:value}} | db.users.find({age:{$gte:23}}).pretty(); | where age>=23 |
| 不等于 | {key:{$ne:value}} | db.users.find({age:{$ne:23}}).pretty(); | where age!=23 |
AND
db.user.find({key1:value1,key2:value2...}).pretty()
db.users.find({name:'Chris', age:33}) --查询 name=Chris 并且 age=33 的文档
db.users.find({age:12, age:{$gt:33}}) --查询 age=12 并且 age>=33 的文档,相同字段出现多次时会以最后一次出现为准
OR
db.users.find({ $or:[ {key1:value1},{key2:value2}... ] });
--查询 name=John 或者age>33 的文档
db.users.find({
$or:[
{name:'John'},{age:{$gt:33}}
]
});
--结果
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John", "age" : 23 }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca", "age" : 64 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b37"), "name" : "John", "age" : 23 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b38"), "name" : "Rebecca", "age" : 64 }
{ "_id" : 9, "name" : "Chris_9", "age" : 41 }
{ "_id" : ObjectId("638e007512b0b293386f7153"), "age" : 45 }
AND 和 OR联合
db.users.find({key1:value, $or:[ {key1:value1},{key2:value2}... ] });
--查询 age>33 的并且名字是 John或者Rebecca 的文档
db.users.find({
{age:{$gt:33},
$or:[
{name:'John'},{name:'Rebecca'}
]
});
> db.users.find({name:{$in:['John','Rebecca']}})
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John", "age" : 31 }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca", "age" : 42 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b37"), "name" : "John", "age" : 23 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b38"), "name" : "Rebecca", "age" : 64 }
--测试数据
db.users.insert([{ name : "Ethan", age : 33, birthday : "2020-03-12 11:27:11", likes : [ "read", "play toy", "running"]},
name : "Dave", age : 23, birthday : "2010-03-12 11:27:11", likes : [ "read", "girl", "running" ]]);
--查询likes字段有running的文档
db.users.find({likes:"running"})
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave", "age" : 23, "birthday" : "2010-03-12 11:27:11", "likes" : [ "read", "girl", "running" ] }
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan", "age" : 33, "birthday" : "2020-03-12 11:27:11", "likes" : [ "read", "play toy", "running" ] }
--查询likes字段只有read和running的文档
db.users.find({likes:["read","running"]})
--查询likes字段长度为3的文档
db.users.find({likes:{$size:3}})
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave", "age" : 23, "birthday" : "2010-03-12 11:27:11", "likes" : [ "read", "girl", "running" ] }
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan", "age" : 33, "birthday" : "2020-03-12 11:27:11", "likes" : [ "read", "play toy", "running" ] }
在mongodb中的模糊查询是通过正则表达式来实现的
--查询名字中包含Chris的文档
db.users.find({name:/Chris/})
--查询名字以Chris开头的文档
db.users.find({name:/^Chris/})
--查询爱好字段中有toy的文档
db.users.find({likes:/toy/})
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan", "age" : 33, "birthday" : "2020-03-12 11:27:11", "likes" : [ "read", "play toy", "running" ] }
1 : 升序, -1 : 降序
--按名字降序,如果名字相同则按年龄升序排列
db.users.find().sort({name:-1,age:1})
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca", "age" : 42 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b38"), "name" : "Rebecca", "age" : 64 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b37"), "name" : "John", "age" : 23 }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John", "age" : 31 }
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan", "age" : 33, "birthday" : "2020-03-12 11:27:11", "likes" : [ "read", "play toy", "running" ] }
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave", "age" : 23, "birthday" : "2010-03-12 11:27:11", "likes" : [ "read", "girl", "running" ] }
{ "_id" : 9, "name" : "Chris_9", "age" : 41 }
{ "_id" : 8, "name" : "Chris_8", "age" : 40 }
{ "_id" : 7, "name" : "Chris_7", "age" : 39 }
{ "_id" : 6, "name" : "Chris_6", "age" : 38 }
{ "_id" : 5, "name" : "Chris_5", "age" : 37 }
{ "_id" : 4, "name" : "Chris_4", "age" : 36 }
{ "_id" : 3, "name" : "Chris_3", "age" : 35 }
{ "_id" : 2, "name" : "Chris_2", "age" : 34 }
{ "_id" : 1, "name" : "Chris_1", "age" : 33 }
{ "_id" : 0, "name" : "Chris_0", "age" : 31 }
类似于sql中的
limit start, rowsstart 从 0 开始 :
算法:(页数 -1)x 显示条数
db.users.find().sort({name:-1,age:1}).skip(start).limit(rows);
-- 第1页
db.users.find().sort({name:-1,age:1}).skip(0).limit(2);
-- 第2页
db.users.find().sort({name:-1,age:1}).skip(2).limit(2);
-- 第3页
db.users.find().sort({name:-1,age:1}).skip(4).limit(2);
-- 第4页
db.users.find().sort({name:-1,age:1}).skip(6).limit(2);
类似于
select count(*) from T where name='Rebecca'
db.users.count()
db.users.find({name:'Rebecca').count();
db.users.distinct('age')
[ 23, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 45, 64 ]
1: 返回该字段
0:不返回该字段
注意
1 和 0 不能同时使用
--默认返回所有字段
> db.users.find({},{});
--查询结果只返回name字段, 注意 _id是默认都返回的,
> db.users.find({},{name:1});
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John" }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca" }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b37"), "name" : "John" }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b38"), "name" : "Rebecca" }
{ "_id" : ObjectId("638df7174d72a1b3fad46b39") }
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave" }
{ "_id" : 0, "name" : "Chris_0" }
{ "_id" : 1, "name" : "Chris_1" }
{ "_id" : 2, "name" : "Chris_2" }
{ "_id" : 3, "name" : "Chris_3" }
{ "_id" : 4, "name" : "Chris_4" }
{ "_id" : 5, "name" : "Chris_5" }
{ "_id" : 6, "name" : "Chris_6" }
{ "_id" : 7, "name" : "Chris_7" }
{ "_id" : 8, "name" : "Chris_8" }
{ "_id" : 9, "name" : "Chris_9" }
{ "_id" : ObjectId("638e007512b0b293386f7153") }
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan" }
--查询时除过_id字段外,其它字段的0和1不能同时使用
> db.users.find({},{_id:1,age:0})
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John" }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca" }
> db.users.find({},{_id:1,age:1})
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "age" : 31 }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "age" : 42 }
> db.users.find({},{name:1,age:0})
Error: error: {
"ok" : 0,
"errmsg" : "Cannot do exclusion on field age in inclusion projection",
"code" : 31254,
"codeName" : "Location31254"
}
$type操作符是基于Bson类型来检索集合中配置的数据类型并返回结果当文档中同一个字段的类型不同时可以用
$type进行过滤https://www.mongodb.com/docs/v5.0/reference/operator/query/type/#mongodb-query-op.-type
Bson type
https://www.mongodb.com/docs/v5.0/reference/bson-types/
Mongodb中的数据类型如下
| Type | Number | Alias | Notes |
|---|---|---|---|
| Double | 1 | "double" | mongodb中数字默认都是double类型 |
| String | 2 | "string" | |
| Object | 3 | "object" | |
| Array | 4 | "array" | |
| Binary data | 5 | "binData" | |
| Undefined | 6 | "undefined" | Deprecated. |
| ObjectId | 7 | "objectId" | |
| Boolean | 8 | "bool" | |
| Date | 9 | "date" | |
| Null | 10 | "null" | |
| Regular Expression | 11 | "regex" | |
| DBPointer | 12 | "dbPointer" | Deprecated. |
| JavaScript | 13 | "javascript" | |
| Symbol | 14 | "symbol" | Deprecated. |
| JavaScript code with scope | 15 | "javascriptWithScope" | Deprecated in MongoDB 4.4. |
| 32-bit integer | 16 | "int" | |
| Timestamp | 17 | "timestamp" | |
| 64-bit integer | 18 | "long" | |
| Decimal128 | 19 | "decimal" | |
| Min key | -1 | "minKey" | |
| Max key | 127 | "maxKey" |
查询数据的具体的Bson type类型
| Example | Results |
|---|---|
{ $type: "a" } | "string" |
{ $type: /a/ } | "regex" |
{ $type: 1 } | "double" |
{ $type: NumberLong(627) } | "long" |
{ $type: { x: 1 } } | "object" |
{ $type: [ [ 1, 2, 3 ] ] } | "array" |
--查询age类型为string的数据
> db.users.find({age:{$type:2}})
> db.users.find({age:{$type:'string'}})
{ "_id" : ObjectId("639d9d49b5512c2b26224951"), "name" : "Ethan", "age" : "3", "birthday" : "2020-03-12 11:27:11", "likes" : [ "read", "play toy", "running" ] }
--查询age类型为double的数据
> db.users.find({age:{$type:1}})
> db.users.find({age:{$type:'double'}})
{ "_id" : ObjectId("638df6d54d72a1b3fad46b35"), "name" : "John", "age" : 31 }
{ "_id" : ObjectId("638df6d54d72a1b3fad46b36"), "name" : "Rebecca", "age" : 42 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b37"), "name" : "John", "age" : 23 }
{ "_id" : ObjectId("638df6fb4d72a1b3fad46b38"), "name" : "Rebecca", "age" : 64 }
{ "_id" : ObjectId("638df7174d72a1b3fad46b39"), "age" : 31 }
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave", "age" : 23, "birthday" : "2010-03-12 11:27:11", "likes" : [ "read", "girl", "running" ] }
{ "_id" : 0, "name" : "Chris_0", "age" : 31 }
{ "_id" : 1, "name" : "Chris_1", "age" : 33 }
{ "_id" : 2, "name" : "Chris_2", "age" : 34 }
{ "_id" : 3, "name" : "Chris_3", "age" : 35 }
{ "_id" : 4, "name" : "Chris_4", "age" : 36 }
{ "_id" : 5, "name" : "Chris_5", "age" : 37 }
{ "_id" : 6, "name" : "Chris_6", "age" : 38 }
{ "_id" : 7, "name" : "Chris_7", "age" : 39 }
{ "_id" : 8, "name" : "Chris_8", "age" : 40 }
{ "_id" : 9, "name" : "Chris_9", "age" : 41 }
{ "_id" : ObjectId("638e007512b0b293386f7153"), "age" : 45 }
db.集合名称.remove(
<query>,
{
justOne:<boolean>,
writeConcern:<document>
}
)
query:
可选,删除文档的条件justOne:
可选,如果设置成true或1,则只删除一个文档,如果不设置或使用默认值false,则删除所有匹配条件的文档。writeConcern:
可选,抛出异常的级别
db.users.remove({}) --删除所有文档
db.users.remove({name:'Chris'}) --删除 name=chris 的文档
> db.users.find()
{ "_id" : ObjectId("638c28e34ce7551b9100bb94"), "name" : "chris" }
db.users.remove({_id:ObjectId("638c28e34ce7551b9100bb94")});--删除自动生成的_id是638c28e34ce7551b9100bb94的文档
db.集合名称.update(
<query>,
<update>,
{
upsert:<boolean>,
multi:<boolean>,
writeConcern:<document>
}
)
query: 更新文档的查询条件,类型sql update中where后面的条件
update: update的对象和一些更新的操作符,如(,inc...)等,可以理解为sql update 中set中的内容
upsert:
可选,如查更新的文档不存在则插入一个,默认为falsemulti:
可选,默认是false,只更新找到的第一条记录,如果设置成true则更新按条件查询出来的全部记录writeConcern:
可选,抛出异常的级别
--会先将 age=32 的删除掉,再插入一条 age=31 的新记录
db.users.update({age:32},{age:31});
--如果要保存原来的记录需要加 $set, 更新的时候也可以新增字段
db.users.update({age:32},{$set:{age:31}});
db.users.update({name:'John'},{$set:{age:41}});
--更新的时候也可以新增字段
db.users.update({age:22},{$set:{age:23, likes:['read','girl','running']}});
--结果
{ "_id" : ObjectId("638df7174d72a1b3fad46b3a"), "name" : "Dave", "age" : 23, "birthday" : "2010-03-12 11:27:11", "likes" : [ "read", "girl", "running" ] }
--将 age=32 的所有记录的age更新为45,如果没有配置到的记录则插入一条新的记录
db.users.update({age:32},{$set:{age:45}},{multi:true,upsert:true});
--结果
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("638e007512b0b293386f7153")
})
> db.users.find()
{ "_id" : ObjectId("638e007512b0b293386f7153"), "age" : 45 }
https://www.mongodb.com/docs/v5.0/indexes/
索引是一种特殊的数据结构,存储在一个易于遍历读取的数据集合中,是对数据库中一列或多列值进行排序的一种结构。
如果没有索引,mongodb在读取数据时需要扫描集合中的每个文档,并选取那些符合查询条件的记录,这种扫描全集合的查询效率是非常低的,特别是在处理大量数据时。
mongodb在集合层面上定义了索引,并支持对mongodb集合中的任何字段或文档的子字段进行索引。
默认
_id索引: 在创建集合时,mongodb会自动在_id字段上创建唯一索引._id字段防止写入两个·_id一样的文档,并且_id上的唯一索引是不能删除的
原理
--key值为你要创建索引的字段, 1:指定按升序创建索引,-1:指定按降序来创建索引
db.users.createIndex(keys, options)
--在字段name和age字段上创建索引,在name字段升序创建索引,当age字段相同时,再在age字段按降序创建索引
db.users.createIndex({name:1, age:-1})
--创建索引时,指定索引名称为 name_age_inx,且为唯一索引
> db.users.createIndex({name:1, age:-1},{name:'name_age_inx', unique:true});
{
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"createdCollectionAutomatically" : false,
"ok" : 1
}
> db.users.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"name" : 1,
"age" : -1
},
"name" : "name_age_inx",
"unique" : true
}
]
--创建索引时,指定索引名称为,且指定过期时间为10秒
> db.users.createIndex({name:1},{expireAfterSeconds:10})
{
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"createdCollectionAutomatically" : false,
"ok" : 1
}
> db.users.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"name" : 1,
"age" : -1
},
"name" : "name_age_inx",
"unique" : true
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"expireAfterSeconds" : 10
}
]
| Parameter | type | Desc |
|---|---|---|
background | Booelean | 【可选】默认为false, 索引创建时会阻塞其它数据库操作,可以指定后台创建。 |
unique | Boolean | 【可选】默认为false, 是否创建唯一索引 |
name | String | 【可选】索引名称, 如果不指定,默认按照字段名称+排序顺序生成一个名称 |
expireAftertSeconds | Integer | 【可选】索引过期时间, 单位为秒,只能指定在单字段索引的过期时间 |
v | index version | 索引的版本号,默认取决于mongodb创建索引时运行的版本 |
--查询一个集合里面的索引
> db.users.getIndexes()
[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
--查询集合索引大小
db.users.totalIndexSize()
36864 --字节
--删除集合中所有的索引
db.users.dropIndexes()
--删除集合中名称为'name_index'的索引
db.users.dropIndex('name_index')
复合索引生效和mysql一样,适用左包含原则,
https://docs.mongodb.com/manual/tutorial/sort-results-with-indexes/#std-lable-sort-on-multiple-fields
if the sort keys correspond to the index keys or an index prefix, MongoDB can use the index to sort the query results. A prefix of a compound index is a subset that consists of one or more keys at the start of the index key pattern.
For example, create a compound index on the data collection:
db.data.createIndex( { a:1, b: 1, c: 1, d: 1 } )
Then, the following are prefixes for that index:
{ a: 1 }
{ a: 1, b: 1 }
{ a: 1, b: 1, c: 1 }
{ a: 1, b: 1, c: 1, d: 1 }
b a c d 也是可以走复合索引的同,
如果检索的字段包含复合索引的全部字段但是顺序不同,引擎会自动优化来使用复合索引
mongodb中排序也是可以使用索引的
The following query and sort operations use the index prefixes to sort the results. These operations do not need to sort the result set in memory.
| Example | Index Prefix |
|---|---|
db.data.find().sort( { a: 1 } ) | { a: 1 } |
db.data.find().sort( { a: -1 } ) | { a: 1 } |
db.data.find().sort( { a: 1, b: 1 } ) | { a: 1, b: 1 } |
db.data.find().sort( { a: -1, b: -1 } ) | { a: 1, b: 1 } |
db.data.find().sort( { a: 1, b: 1, c: 1 } ) | { a: 1, b: 1, c: 1 } |
db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } ) | { a: 1, b: 1 } |
mongodb中的聚合主要用于处理数据,如统计平均值,求和等,并返回计算后的数据结果,类似于mysql中的count()
| 创建时间: | 2022/12/11 23:11 |
| 更新时间: | 2022/12/17 17:23 |
| 作者: | Chris |
package com.chris.cloud.importer.bean;
public interface ServiceInterface {
void test();
}
package com.chris.cloud.importer.bean.impl;
import com.chris.cloud.importer.bean.ServiceInterface;
public class ServiceA implements ServiceInterface {
public ServiceA() {
System.out.println("new service A");
}
@Override
public void test() {
System.out.println("ServiceA");
}
}
package com.chris.cloud.importer.bean.impl;
import com.chris.cloud.importer.bean.ServiceInterface;
public class ServiceB implements ServiceInterface {
public ServiceB() {
System.out.println("new service B");
}
@Override
public void test() {
System.out.println("ServiceB");
}
}
package com.chris.cloud.importer.bean.impl;
import cn.hutool.json.JSONUtil;
import com.chris.cloud.importer.bean.impl.ServiceB;
import com.chris.cloud.importer.config.ConfigB;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
@Slf4j
@SuppressWarnings({"NullableProblems"})
public class ServiceImportSelector implements ImportSelector {
/**
* 通过AnnotationMetadata里面的属性,动态加载类。AnnotationMetadata是Import注解所在的类属性(如果所在类是注解类,则延伸至应用这个注解类的非注解类为止)
*
* @param importingClassMetadata com.chris.cloud.importer.config.ConfigA
* @return 返回要加载的@Configuation或者具体Bean类的全限定名的String数组
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//可以是@Configuration注解修饰的类,也可以是具体的Bean类的全限定名称
String[] beanClzNames = new String[]{ConfigB.class.getName()/*, ServiceB.class.getName()*/};
log.info("beanClzNames:{}", JSONUtil.toJsonStr(beanClzNames));
return beanClzNames;
}
}
package com.chris.cloud.importer.bean.impl;
import com.chris.cloud.importer.bean.EnableImportService;
import com.chris.cloud.importer.utils.ImportUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.AnnotationMetadata;
import java.util.Map;
import java.util.Set;
@Slf4j
@SuppressWarnings({"NullableProblems"})
public class ServiceImportSelector2 implements ImportSelector {
/**
* 通过AnnotationMetadata里面的属性,动态加载类。AnnotationMetadata是Import注解所在的类属性(如果所在类是注解类,则延伸至应用这个注解类的非注解类为止)
*
* @param importingClassMetadata com.chris.cloud.importer.config.ConfigA
* @return 返回要加载的@Configuation或者具体Bean类的全限定名的String数组
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
MergedAnnotations annotations = importingClassMetadata.getAnnotations();
Set<String> annotationTypes = importingClassMetadata.getAnnotationTypes();
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableImportService.class.getName());
Map<String, Object> annoMap = importingClassMetadata.getAnnotationAttributes(EnableImportService.class.getName(), true);
return ImportUtil.getClzNames(annoMap);
}
}
package com.chris.cloud.importer.bean;
import com.chris.cloud.importer.bean.impl.ServiceImportSelector2;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import(ServiceImportSelector2.class)
@SuppressWarnings({"rawtypes"})
public @interface EnableImportService {
Class[] clz_names();
}
public class User1 {
private String name;
}
public class User2 {
private String name;
}
package com.chris.cloud.importer.config;
import com.chris.cloud.importer.bean.EnableImportService;
import com.chris.cloud.importer.bean.ServiceInterface;
import com.chris.cloud.importer.bean.impl.ServiceA;
import com.chris.cloud.importer.bean.impl.ServiceB;
import com.chris.cloud.importer.bean.impl.ServiceImportSelector;
import com.chris.cloud.importer.bean.impl.User1;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Import(ConfigB.class)
//Spring 4.2之后,@Import可以直接指定实体类,加载这个类定义到context中, @Import(ServiceB.class),就会生成ServiceB的Bean到容器上下文中
//@Import(ServiceB.class)
//通过mportSelector实现动态加载,需要返回要加载的@Configuation或者具体Bean类的全限定名的String数组
//@Import(ServiceImportSelector.class)
//@EnableImportService(clz_names = {ConfigB.class})
@Configuration
@Slf4j
public class ConfigA {
ConfigA() {
log.info("begin get configA");
}
@Bean
public Object getUser1() {
log.info("begin get User1");
return new User1();
}
@Bean
@ConditionalOnMissingBean
public ServiceInterface getServiceA() {
log.info("begin get service A");
return new ServiceA();
}
}
package com.chris.cloud.importer.config;
import com.chris.cloud.importer.bean.impl.ServiceB;
import com.chris.cloud.importer.bean.ServiceInterface;
import com.chris.cloud.importer.bean.impl.User2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
public class ConfigB {
ConfigB() {
log.info("begin get configB");
}
@Bean
public Object getUser2() {
log.info("begin get User2");
return new User2();
}
@Bean
@ConditionalOnMissingBean
public ServiceInterface getServiceB() {
log.info("begin get service B");
return new ServiceB();
}
}
package com.chris.cloud;
@SpringBootTest(classes = Cloud2022Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
public class ImportTest implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Test
public void test2() {
// AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigA.class);
ServiceInterface bean = this.applicationContext.getBean(ServiceInterface.class);
bean.test();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
| 创建时间: | 2020/9/2 15:43 |
| 更新时间: | 2022/12/14 23:11 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/forezp/article/details/84313907 |
按 byType 自动注入
@Autowired默认按类型装配(这个注解是属于spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,
如:@Autowired(required=false),如果我们想使用名称装配可以结合@Qualifier注解进行使用
@Autowired()
@Qualifier("baseDao")
private BaseDao baseDao;
默认按 byName 自动注入
默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配
@Resource(name="baseDao")
private BaseDao baseDao;
@Resource 装配顺序
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
- 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到,再根据类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常则抛出异常
- 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到会抛出异常,找到多个,会根据默认取字段名进行名称匹配,匹配不到则报错。
- 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;
@Autowired是根据类型进行自动装配的。
- 如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;
@Autowired
@Qualifier("userServiceImpl")
public IUserService userService;
@Autowired
public void setUserDao(@Qualifier("userDao") UserDao userDao) {
this.userDao = userDao;
}
这样Spring会找到id为userServiceImpl和userDao的bean进行装配。
- 如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。
@Autowired(required = false)
public IUserService userService
定义控制层Bean,如Action
@Controller
@Controller("Bean的名称")
定义业务层Bean
@Service
@Service("Bean的名称")
定义DAO层Bean
@Repository
@Repository("Bean的名称")
定义Bean, 不好归类时使用.
使用注解@Configuration,告诉Spring容器这是一个配置类.
@Bean:给容器中添加组件,以方法名作为组件的id。返回类型为组件类型,返回的值,就是组件在容器中的实例
@Configuration
public class ConfigProxyBeanMethods {
@Bean
public User getUser01() {
return new User();
}
@Bean
public Pet getPet01() {
return new Pet();
}
/*@Bean("oh baby")
public Pet getPet01() {
return new Pet();
}*/
}
/**
* 2022-12-14 22:18:00.558 INFO 5604 --- [ main] com.chris.cloud.ConfigProxyTest : bean name :getUser01
* 2022-12-14 22:18:00.558 INFO 5604 --- [ main] com.chris.cloud.ConfigProxyTest : bean name :getPet01
* <p>
* <p>
* 2022-12-14 22:20:24.551 INFO 11480 --- [ main] com.chris.cloud.ConfigProxyTest : bean name :getUser01
* 2022-12-14 22:20:24.551 INFO 11480 --- [ main] com.chris.cloud.ConfigProxyTest : bean name :oh baby
*/
@Test
public void test1() {
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
log.info("bean name :{}", beanDefinitionName);
}
}
proxyBeanMethods = false
@Configuration(proxyBeanMethods = false)
public class ConfigProxyBeanMethods {
@Bean
public User getUser01() {
return new User();
}
@Bean
public Pet getPet01() {
return new Pet();
}
/*@Bean("oh baby")
public Pet getPet01() {
return new Pet();
}*/
}
/**
* proxyBeanMethods,默认为true
* 容器中注册的组件默认是单实例的
* 在向容器中注册组件时,会在容器中查找有没有该组件。如果有,则取该组件用于保证单实例,如果没有再注册一个新的组件。
* 2022-12-14 23:06:00.966 INFO 17044 --- [ main] com.chris.cloud.ConfigProxyTest : user01.equals(user02): true
* 2022-12-14 23:06:00.967 INFO 17044 --- [ main] com.chris.cloud.ConfigProxyTest : user01 == user02 : true
* 2022-12-14 23:06:00.967 INFO 17044 --- [ main] com.chris.cloud.ConfigProxyTest : getUser01 == user01: true
* <p>
* proxyBeanMethods 改为false
*
* 2022-12-14 23:05:21.110 INFO 4244 --- [ main] com.chris.cloud.ConfigProxyTest : user01.equals(user02): true
* 2022-12-14 23:05:21.110 INFO 4244 --- [ main] com.chris.cloud.ConfigProxyTest : user01 == user02 : true
* 2022-12-14 23:05:21.110 INFO 4244 --- [ main] com.chris.cloud.ConfigProxyTest : getUser01 == user01: false
*/
@Test
public void test2() {
ConfigProxyBeanMethods bean = applicationContext.getBean(ConfigProxyBeanMethods.class);
User getUser01 = bean.getUser01();
User user01 = applicationContext.getBean("getUser01", User.class);
User user02 = applicationContext.getBean("getUser01", User.class);
log.info("user01.equals(user02): {}", user01.equals(user02));
log.info("user01 == user02 : {}", user01 == user02);
log.info("getUser01 == user01: {}", getUser01 == user01);
}
定义Bean的作用域和生命过程
@Scope("prototype")
值有:singleton,prototype,request,session,globalSession
相当于
init-method, 使用在方法上,当Bean初始化时执行。
相当于
destory-method,使用在方法上,当Bean销毁时执行。
| 创建时间: | 2020/9/8 11:23 |
| 更新时间: | 2022/12/11 10:14 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/sqlgao22/article/details/96476754 |
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
prefix = "xxx":配置文件中哪个下面的所有属性进行一一映射
只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能;
@ConfigurationProperties(prefix = "xxx")默认从全局配置文件中获取值;
@Component
@ConfigurationProperties(prefix = "async.executor.thread")
@Data
public class ThreadPoolConfigBean {
private int corePoolSize;
private int maxPoolSize;
private int queueCapacity;
private String namePrefix;
}
@EnableConfigurationProperties 注解的作用是:使使用 @ConfigurationProperties 注解的类生效.
如果一个配置类只配置
@ConfigurationProperties注解,而没有使用@Component,那么在IOC容器中是获取不到properties 配置文件转化的bean。说白了 @EnableConfigurationProperties 相当于把使用 @ConfigurationProperties 的类进行了一次注入。
@Configuration
@EnableConfigurationProperties(ThreadPoolConfigBean.class)
public class ExecutorConfig {
@Resource
private ThreadPoolConfigBean configBean;
可以用来替换@EnableConfigurationProperties,配置在主启动类上对项目中的属性配
置类统一扫描后生成bean放到容器中
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan("com.chris.mybatisplus.dao.mapper")
@ConfigurationPropertiesScan("com.chris.mybatisplus.config")
@EnableAsync
public class MybatisPlusMain {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusMain.class, args);
}
}
/** 这里加了@ConditionalOnBean注解,就代表如果city存在才实例化people*/
@Bean
@ConditionalOnBean(name = "city")
public People (City city) { //这里如果city实体没有成功注入 这里就会报空指针
city.setCityCode(301701);
return new People("小小", 3, city);
}
@Conditional({WindowsCondition.class})
@ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)
@ConditionalOnClass(某个class位于类路径上,才会实例化一个Bean)
@ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)
@ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)
@ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)
@ConditionalOnNotWebApplication(不是web应用)
当前上下文中不存在TeamTalkService对象时,才会实例化一个TeamTalkService Bean
@Bean
@ConditionalOnMissingBean(TeamTalkService.class)
public TeamTalkService teamTalkService() {
return new TeamTalkService(prefix, appId, secret);
}
Spring Boot通过@ConditionalOnProperty来控制Configuration是否生效
通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值。
如果该值为空,则返回false;
如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。
如果返回值为false,则该configuration不生效;为true则生效。
//控制某个configuration是否生效
@Configuration
@ConditionalOnProperty(prefix = "filter",name = "loginFilter",havingValue = "true",
matchIfMissing=false)
public class FilterConfig {
// prefix 为配置文件中的前缀,
// name 为配置的名字
// havingValue 是与配置的值对比值,当两个值相同返回true,配置类生效.
// matchIfMissing 缺少该property时是否可以加载。如果为true,没有该property也会正常加载;反之报错
@Bean
public FilterRegistrationBean getFilterRegistration() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean(new LoginFilter());
filterRegistration.addUrlPatterns("/*");
return filterRegistration; }
}
配置文件中的代码
filter.loginFilter=true
| 创建时间: | 2022/5/10 19:00 |
| 更新时间: | 2022/12/11 9:51 |
| 作者: | Chris |
IOC 的核心就是原先创建一个对象,我们需要自己直接通过
new来创建,而 IOC 就相当于有人帮们创建好了对象,需要使用的时候直接去拿就行
IOC 主要有两种实现方式:
DL(Dependency Lookup):依赖查找
DI(Dependency Inject):依赖注入
DL(Dependency Lookup)
容器帮我们创建好了对象,我们需要使用的时候自己再主动去容器中查找
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml");
Object bean = applicationContext.getBean("object");
DI(Dependency Inject)
依赖注入相比较依赖查找又是一种优化,也就是我们不需要自己去查找,只需要告诉容器当前需要注入的对象,容器就会自动将创建好的对象进行注入(赋值)
@Service
public class UserService {
@Autowired
private Wolf1Bean wolf1Bean;//通过属性注入
}
@Service
public class UserService {
private Wolf3Bean wolf3Bean;
@Autowired //通过setter方法实现注入
public void setWolf3Bean(Wolf3Bean wolf3Bean) {
this.wolf3Bean = wolf3Bean;
}
}
@Service
public class UserService {
private Wolf2Bean wolf2Bean;
@Autowired //通过构造器注入
public UserService(Wolf2Bean wolf2Bean) {
this.wolf2Bean = wolf2Bean;
}
}
假如我们想要注入一个接口,而当前接口又有多个实现类,那么这时候就会报错,因为 Spring 无法知道到底应该注入哪一个实现类。
比如我们上面的三个类全部实现同一个接口 IWolf,那么这时候直接使用常规的,不带任何注解元数据的注入方式来注入接口 IWolf。
@Autowired
private IWolf iWolf;
解决思路主要有以下 5 种:
通过配置文件和 @ConditionalOnProperty 注解实现
配置文件中配置了
lonely.wolf=test1
@Component
@ConditionalOnProperty(name = "lonely.wolf",havingValue = "test1")
public class Wolf1Bean implements IWolf{
}
这种配置方式,编译器可能还是会提示有多个 Bean,但是只要我们确保每个实现类的条件不一致,就可以正常使用。
@ConditionalOnBean:当存在某一个 Bean 时,初始化此类到容器。
@ConditionalOnClass:当存在某一个类时,初始化此类的容器。
@ConditionalOnMissingBean:当不存在某一个 Bean 时,初始化此类到容器。
@ConditionalOnMissingClass:当不存在某一个类时,初始化此类到容器。
只会注入 BeanName 为 wolf1Bean 的实现类
@Component
public class InterfaceInject {
@Resource(name = "wolf1Bean")
private IWolf iWolf;
}
可以通过集合的方式一次性注入接口的所有实现类
这两种形式都会将 IWolf 中所有的实现类注入集合中。
如果使用的是 List 集合,那么我们可以取出来再通过instanceof关键字来判定类型;
通过 Map 集合注入的话,Spring 会将 Bean 的名称默认类名首字母小写作为key来存储,这样我们就可以在需要的时候动态获取自己想要的实现类。
@Component
public class InterfaceInject {
@Autowired
List<IWolf> list;
@Autowired
private Map<String,IWolf> map;
}
在其中某一个实现类上加上
@Primary注解来表示当有多个 Bean 满足条件时,优先注入当前带有@Primary注解的 Bean
@Component
@Primary
public class Wolf1Bean implements IWolf{
}
@Component
public class InterfaceInject {
//注入
@Autowired
private ApplicationContext applicationContext;
public Object getBean(){
//获取bean
return applicationContext.getBean("wolf2.ean");
}
}
通过实现ApplicationContextAware接口来获取ApplicationContext对象,从而获取 Bean。
需要注意的是,实现ApplicationContextAware接口的类也需要加上注解,以便交给 Spring 统一管理(这种方式也是项目中使用比较多的一种方式)
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 通过名称获取bean
*/
public static <T> T getBeanByName(String beanName){
return (T) applicationContext.getBean(beanName);
}
/**
* 通过类型获取bean
*/
public static <T>T getBeanByType(Class<T> clazz){
return (T) applicationContext.getBean(clazz);
}
}
Wolf2Bean wolf2Bean = SpringContextUtil.getBeanByName("wolf2Bean");
Wolf3Bean wolf3Bean = SpringContextUtil.getBeanByType(Wolf3Bean.class);
这两个对象中,WebApplicationObjectSupport继承了ApplicationObjectSupport,所以并无实质的区别。
@Component
public class SpringUtil extends /*WebApplicationObjectSupport*/ ApplicationObjectSupport {
private static ApplicationContext applicationContext = null;
public static <T>T getBean(String beanName){
return (T) applicationContext.getBean(beanName);
}
@PostConstruct
public void init(){
applicationContext = super.getApplicationContext();
}
}
//有了工具类,在方法中就可以直接调用了:
@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
@GetMapping("/bean3")
public Object getBean3(){
Wolf2.ean wolf2.ean = SpringUtil.getBean("wolf2.ean");
return wolf2.ean.toString();
}
}
通过HttpServletRequest对象,再结合 Spring 自身提供的工具类
WebApplicationContextUtils也可以获取到ApplicationContext对象,
而HttpServletRequest对象可以主动获取(如下 getBean2 方法),也可以被动获取(如下 getBean2.方法):
@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
@GetMapping("/bean2.)
public Object getBean2.HttpServletRequest request){
//直接通过方法中的HttpServletRequest对象
ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
Wolf2.ean wolf2.ean = (Wolf2.ean)applicationContext.getBean("wolf2.ean");
return wolf2.ean.toString();
}
@GetMapping("/bean2")
public Object getBean2(){
//手动获取request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
Wolf2Bean wolf2Bean = (Wolf2Bean)applicationContext.getBean("wolf2Bean");
return wolf2Bean.toString();
}
}
| 创建时间: | 2020/9/2 15:31 |
| 更新时间: | 2022/12/8 22:40 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/u010406047/article/details/110878472 |
mvn dependency:tree
mvn dependency:tree -Dverbose -Dincludes=commons-collections
idea 可以安装 maven helper 插件
Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
D:\apache-maven-3.6.3\conf\settings.xml
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
<mirrorOf>central</mirrorOf>
</mirror>
spring-boot-maven-plugin是spring boot提供的maven打包插件。可打直接可运行的jar包或war包。
https://docs.spring.io/spring-boot/docs/2.2.1.RELEASE/maven-plugin/
| Goal | Description |
|---|---|
| spring-boot:build-info | Generate a build-info.properties file based the content of the current MavenProject. |
| spring-boot:help | Display help information on spring-boot-maven-plugin. Call mvn spring-boot:help -Ddetail=true -Dgoal= to display parameter details. |
| spring-boot:repackage | Repackages existing JAR and WAR archives so that they can be executed from the command line using java -jar. With layout=NONE can also be used simply to package a JAR with nested dependencies (and no main class, so not executable). |
| spring-boot:run | Run an executable archive application. |
| spring-boot:start | Start a spring application. Contrary to the run goal, this does not block and allows other goal to operate on the application. This goal is typically used in integration test scenario where the application is started before a test suite and stopped after. |
| spring-boot:stop | Stop a spring application that has been started by the "start" goal. Typically invoked once a test suite has completed. |
打包:repackage
打包主要使用的是repackage goal,它是spring-boot-starter-parent为插件设置的默认goal。这个goal绑定在maven的 package生命周期上,完整命令为mvn package spring-boot:repackage。在 mvn package 执行打包之后,repackage 再次打包生成可执行的 jar包或war包.
<!-- 打源码包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
| 创建时间: | 2022/5/26 17:29 |
| 更新时间: | 2022/12/8 21:54 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/qgnczmnmn/article/details/118050472 |
Maven中的scope主要有以下6种,接下来分别介绍下这几种Scope:
不声明scope元素的情况下的 默认值;
compile表示被依赖包需要参与当前项目的编译,包括后续的测试,运行周期也参与其中,是一个比较强的依赖;
打包的时候通常需要包含进去。
provided 类型的scope只会在项目的
编译,测试阶段起作用;可以认为在目标容器中已经提供了这个依赖,无需在提供,但是在编写代码或者编译时可能会用到这个依赖;
依赖不会被打入到项目jar包中
说到provided,这里就要说到<dependency>下的子标签<optional> ,如果一个依赖的<optional> 设置为true,则该依赖在打包的时候不会被打进jar包,同时不会通过依赖传递传递到依赖该项目的工程;
例如:x依赖B,B由依赖于A(x->B->A),则A中设置<optional> 为true的依赖不会被传递到x中。
这两者的区别在于:
1、<optional>为true 表示某个依赖可选,该依赖是否使用都不会影响服务运行;
2、provided的<scope>在目标容器中已经提供了这个依赖,无需在提供
runtime 与 compile 比较相似,区别在于
runtime跳过了编译阶段,打包的时候通常需要包含进去。
在一般的编译和运行时都不需要,它们只有在
测试编译和测试运行阶段可用,
不会被打包到项目jar包中,同时如果项目A依赖于项目B,项目B中的
test作用域下的依赖不会被继承。
表示使用本地系统路径下的jar包,需要和一个systemPath一起使用,如下:
<dependency>
<groupId>xxxx</groupId>
<artifactId>xxx</artifactId>
<systemPath>${basedir}/lib/xxxxx.jar</systemPath>
<scope>system</scope>
<version>1.4.12</version>
</dependency>
import 只能在pom文件的
<dependencyManagement>中使用,从而引入其他的pom文件中引入依赖,如:在Spring boot 项目的POM文件中,我们可以通过在POM文件中继承 Spring-boot-starter-parent来引用Spring boot默认依赖的jar包,如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.BUILD-SNAPSHOT</version>
</parent>
但是,通过上面的parent继承的方法,只能继承一个 spring-boot-start-parent。
实际开发中,用户很可能需要继承自己公司的标准parent配置,这个时候可以使用 scope=import 来实现多继承。代码如下:
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.1.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
通过上面方式,就可以获取spring-boot-dependencies.2.0.1.BUILD-SNAPSHOT.pom文件中dependencyManagement配置的jar包依赖。
如果要继承多个,可以在dependencyManagement中添加,如:
<dependencyManagement>
<dependencies>
<!-- Override Spring Data release train provided by Spring Boot -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-releasetrain</artifactId>
<version>Fowler-SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.1.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
| 创建时间: | 2022/12/7 23:16 |
| 更新时间: | 2022/12/8 12:19 |
| 作者: | Chris |
package com.chris.guava.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class CacheTest {
/**
* 通过CacheBuilder类构建一个缓存对象,CacheBuilder类采用builder设计模式,它的每个方法都返回CacheBuilder本身,直到build方法被调用
*/
@Test
public void test1() {
Cache<String, String> cache = CacheBuilder.newBuilder().build();
cache.put("word", "Hello Guava Cache");
System.out.println(cache.getIfPresent("word"));
}
/**
* 设置最大存储
* Guava Cache可以在构建缓存对象时指定缓存所能够存储的最大记录数量。
* 当Cache中的记录数量达到最大值后,再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中选择一条删除掉,腾出空间后,再将新的对象存储到Cache中。
* 第一个值:null
* 第二个值:value2
* 第三个值:value3
*/
@Test
public void test2() {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
System.out.println("第一个值:" + cache.getIfPresent("key1"));
System.out.println("第二个值:" + cache.getIfPresent("key2"));
System.out.println("第三个值:" + cache.getIfPresent("key3"));
}
/**
* 设置过期时间
* <p>
* 可以通过CacheBuilder类的expireAfterAccess和expireAfterWrite两个方法为缓存中的对象指定过期时间
* 使用CacheBuilder构建的缓存不会“自动”执行清理和逐出值,也不会在值到期后立即执行或逐出任何类型。
* 相反,它在写入操作期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。
* 其中,expireAfterWrite方法指定对象被写入到缓存后多久过期,expireAfterAccess方法指定对象多久没有被访问后过期。
* <p>
* 第1次取到key1的值为:value1
* 第2次取到key1的值为:value1
* 第3次取到key1的值为:value1
* 第4次取到key1的值为:null
* 第5次取到key1的值为:null
*/
@Test
public void test3() throws InterruptedException {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
int time = 1;
while (time <= 5) {
System.out.println("第" + time++ + "次取到key1的值为:" + cache.getIfPresent("key1"));
Thread.sleep(1000);
}
}
/**
* 通过CacheBuilder的expireAfterAccess方法,指定如果Cache中存储的对象超过3秒没有被访问,就会过期。
* while中的代码每sleep一段时间,就会访问一次Cache中存储的对象key1;每次访问key1之后,下次sleep的时间会加长一秒。
* 也可以同时用expireAfterAccess和expireAfterWrite方法指定过期时间,这时只要对象满足两者中的一个条件,就会被自动过期删除。
* <p>
* 睡眠1秒后取到key1的值为:value1
* 睡眠2秒后取到key1的值为:value1
* 睡眠3秒后取到key1的值为:null
* 睡眠4秒后取到key1的值为:null
* 睡眠5秒后取到key1的值为:null
*/
@Test
public void test4() throws InterruptedException {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
int time = 1;
while (time <= 5) {
Thread.sleep(time * 1000L);
System.out.println("睡眠" + time++ + "秒后取到key1的值为:" + cache.getIfPresent("key1"));
}
}
/**
* 显示清除缓存
* <p>
* 调用Cache的invalidateAll或invalidate方法,可以显示删除Cache中的记录。
* invalidate方法一次只能删除Cache中一个记录,接收的参数是要删除记录的key。
* invalidateAll方法可以批量删除Cache中的记录;当没有传任何参数时,invalidateAll方法将清除Cache中的全部记录。
* <p>
* cache size:4
* invalidate key1 cache size:3
* invalidate key2 key3 cache size:1
* invalidate all cache size:0
* null
* null
* null
*/
@Test
public void test5() {
Cache<String, String> cache = CacheBuilder.newBuilder().build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
System.out.println("cache size:" + cache.size());
cache.invalidate("key1");
System.out.println("invalidate key1 cache size:" + cache.size());
List<String> list = new ArrayList<>();
list.add("key2");
list.add("key3");
cache.invalidateAll(list); //批量清除list中全部key对应的记录
System.out.println("invalidate key2 key3 cache size:" + cache.size());
cache.invalidateAll();
System.out.println("invalidate all cache size:" + cache.size());
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
System.out.println(cache.getIfPresent("key3"));
}
/**
* 移除动作监听器
* <p>
* 可以为Cache对象添加一个移除监听器。这样,当有记录被删除时,可以感知到这个事件
* <p>
* 这个存在问题,当内存数据清理掉时候,RemovalListener不会触发
* 因为
* Guava并不保证在过期时间到了之后立刻删除该key,如果你此时去访问这这个key,它会检测是否过期,过期则移除.
* 所以过期时间到了之后你去访问这个key会显示这个key已经移除,但是你如果不做任何操作,那么这个key还在内存中。
* <p>
* [key1:value1] is removed!
* [key2:value2] is removed!
* [key3:value3] is removed!
* [key4:value4] is removed!
* [key5:value5] is removed!
*/
@Test
public void test6() {
RemovalListener<String, String> listener = new RemovalListener<String, String>() {
public void onRemoval(RemovalNotification<String, String> notification) {
System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
}
};
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.removalListener(listener)
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.put("key5", "value5");
cache.put("key6", "value6");
cache.put("key7", "value7");
cache.put("key8", "value8");
}
private static final Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.build();
/**
* 自动加载
* <p>
* Cache的get方法有两个参数,第一个参数是从Cache中获取记录的key,第二个记录是一个Callable对象。
* 当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法.
* call方法的返回值会作为“key对应的值”被存储到缓存中,并且被get方法返回。
* <p>
* 有两个线程共享同一个Cache对象,两个线程同时调用get方法获取同一个key对应的值。由于key对应的值不存在,所以两个线程都在get方法处阻塞。
* 此处在call方法中调用Thread.sleep(1000),模拟程序从外存加载数据的时间消耗。
* 虽然是两个线程同时调用get方法,但只有一个get方法中的Callable会被执行(只打印出load1或者load2)。
* Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务,加载数据存到缓存。
* 当加载完数据后,任何线程中的get方法都会获取到key对应的值。
* <p>
* thread1
* thread2
* load2
* thread1 auto load by Callable 2
* thread2 auto load by Callable 2
*/
@Test
public void test7() throws InterruptedException {
new Thread(() -> {
System.out.println("thread1");
try {
String value = cache.get("key", () -> {
System.out.println("load1"); //加载数据线程执行标志
Thread.sleep(1000); //模拟加载时间
return "auto load by Callable 1";
});
System.out.println("thread1 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("thread2");
try {
String value = cache.get("key", () -> {
System.out.println("load2"); //加载数据线程执行标志
Thread.sleep(1000); //模拟加载时间
return "auto load by Callable 2";
});
System.out.println("thread2 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(5);
}
/**
* 统计信息
* <p>
* 可以对Cache的命中率、加载数据时间等信息进行统计。
* 在构建Cache对象时,通过CacheBuilder的recordStats方法,可以开启统计信息的开关。
* 开关开启后,Cache会自动对缓存的各种操作进行统计,调用Cache的stats方法可以查看统计后的信息。
*/
@Test
public void test8() {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.recordStats() //开启统计信息开关
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.getIfPresent("key1");
cache.getIfPresent("key2");
cache.getIfPresent("key3");
cache.getIfPresent("key4");
cache.getIfPresent("key5");
cache.getIfPresent("key6");
System.out.println(cache.stats()); //获取统计信息
}
}
@Data
@AllArgsConstructor
class Customer {
private Integer id;
private String firstName;
}
/**
* @Auther Chris Lee
* @Date 1/3/2019 10:50
* @Description LoadingCache是Cache的子接口,相比较于Cache,当从LoadingCache中读取一个指定key的记录时,如果该记录不存在,则LoadingCache可以自动执行加载数据到缓存的操作.
* 在调用CacheBuilder的build方法时必须传递一个CacheLoader类型的参数,CacheLoader的load方法需要我们提供具体实现。
* 当调用LoadingCache的get方法时,如果缓存不存在对应key的记录,则CacheLoader中的load方法会被自动调用从外存加载数据,“load方法的返回值”会作为“key对应的value”存储到LoadingCache中,并从get方法返回。
*/
public enum CustomerCacheProvider {
INSTANCE;
private final LoadingCache<Integer, Customer> customerCache;
CustomerCacheProvider() {
CacheLoader<Integer, Customer> cacheLoader = new CacheLoader<Integer, Customer>() {
@Override
public Customer load(Integer id) {
return CustomerRepo.findById(id);
}
};
customerCache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(2, TimeUnit.SECONDS).build(cacheLoader);
}
public LoadingCache<Integer, Customer> getCustomerCache() {
return customerCache;
}
}
/**
* @Auther Chris Lee
* @Date 1/3/2019 10:45
* @Description
*/
class CustomerRepo {
private static final Map<Integer, Customer> customers = new HashMap<>();
static {
// add data
Customer customer1 = new Customer(1, "Jhon");
Customer customer2 = new Customer(2, "James");
Customer customer3 = new Customer(3, "Mark");
customers.put(1, customer1);
customers.put(2, customer2);
customers.put(3, customer3);
}
public static Customer findById(Integer id) {
System.out.printf("find %s in Repo \n", id);
if (customers.containsKey(id)) {
return customers.get(id);
}
return null;
}
}
/**
* @Auther Chris Lee
* @Date 1/3/2019 10:52
* @Description https://blog.csdn.net/qq_38567039/article/details/126488902
*/
public class Main {
/**
* 与构建Cache类型的对象类似,LoadingCache类型的对象也是通过CacheBuilder进行构建。
* 不同的是,在调用CacheBuilder的build方法时,必须传递一个CacheLoader类型的参数,CacheLoader的load方法需要我们提供具体实现。
* 当调用LoadingCache的get方法时,如果缓存不存在对应key的记录,则CacheLoader中的load方法会被自动调用从外存加载数据,
* “load方法的返回值”会作为“key对应的value”存储到LoadingCache中,并从get方法返回。
*/
@Test
public void test1() {
LoadingCache<Integer, Customer> cutomerCache = CustomerCacheProvider.INSTANCE.getCustomerCache();
/*
* V get(K key) : This will either return an already cached value, or else use the cache's CacheLoader to atomically
* load a new value into the cache
*/
try {
System.out.println("Cache size : " + cutomerCache.size());
System.out.println("Get Customer 1 : " + cutomerCache.get(1).getFirstName());
System.out.println("Cache size : " + cutomerCache.size());
TimeUnit.SECONDS.sleep(3);
System.out.println("Get Customer 1 : " + cutomerCache.get(1).getFirstName());
System.out.println("Get Customer 3 : " + cutomerCache.get(3).getFirstName());
System.out.println("Cache size : " + cutomerCache.size());
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
| 创建时间: | 2022/12/6 22:54 |
| 更新时间: | 2022/12/6 23:06 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/m0_63989537/article/details/124640643 |
Type体系中类型的包括:原始类型(Class)、参数化类型(ParameterizedType)、数组类型(GenericArrayType)、类型变量(TypeVariable)、基本类型(Class);
- 原始类型,不仅仅包含我们平常所指的类,还包括枚举、数组、注解等;
- 参数化类型,就是我们平常所用到的泛型List、Map;
- 数组类型,并不是我们工作中所使用的数组String[] 、byte[],而是带有泛型的数组,即T[] ;
- 基本类型,也就是我们所说的java的基本类型,即int,float,double等
Type的直接子接口ParameterizedType: 是参数化类型,即泛型,类似List、Map<Integer, String>、List<? extends Number>带有类型参数的类型,也可以是自定义的.
public interface ParameterizedType extends Type {
// 获取泛型的类型,比如 Map<Integer,String> 获取的是 Integer和String
Type[] getActualTypeArguments();
// 获取<> 前面的类型,比如 Map<Integer,String> 获取的是Map
Type getRawType();
// For example, if this type is {@code O<T>.I<S>}, return a representation of {@code O<T>}.
Type getOwnerType();
}
@Data
public class Person {
List<Integer> integerList;
Map<String, Integer> belongs;
}
/**
* integerList 属性的原生类型为 interface java.util.List, 参数类型有:class java.lang.Integer
* belongs 属性的原生类型为 interface java.util.Map, 参数类型有:class java.lang.String class java.lang.Integer
*/
@Test
public void test1() {
Class<Person> clz = Person.class;
Field[] declaredFields = clz.getDeclaredFields();
Arrays.stream(declaredFields).forEach(field -> {
if (field.getGenericType() instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
System.out.printf("%s 属性的原生类型为 %s, 参数类型有:", field.getName(), parameterizedType.getRawType());
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
}
});
}
TypeVariable是各种类型变量的公共父接口,就是泛型里面的类似T、E。
在这需要强调的是,TypeVariable代表着泛型中的变量,而ParameterizedType则代表整个泛型
public interface GenericDeclaration extends AnnotatedElement {
/**
* 返回的泛型变量数组代表了泛型声明的内容,TypeVariable类型就是定义泛型变量的。
*/
public TypeVariable<?>[] getTypeParameters();
}
GenericDeclaration 有三个直接子类
Class, Construtor, Method,也就是说只能在这几种对象上进行范型变量的声明(定义)。
GenericDeclaration的接口方法getTypeParameters用来逐个获取该GenericDeclaration的范型变量声明.
public interface TypeVariable<D extends GenericDeclaration> extends Type, AnnotatedElement {
/**
* 获得该类型变量的上限(上边界),若无显式定义(extends),默认为Object,类型变量的上限可能不止一个,因为可以用&符号限定多个(这其中有且只能有一个为类或抽象类,且必须放在extends后的第一个,即若有多个上边界,则第一个&后必为接口)
* 比如 xxxClass<K extends List & Serialize>,则返回 List和Serialize
*/
Type[] getBounds();
/**
* 获取的是在哪个类上进行的泛型声明,在如上的例子中,返回的是xxxClass
* @since 1.5
*/
D getGenericDeclaration();
/**
* 同样是上面的例子,返回 K, 也就是泛型类型声明时写的名字
*/
String getName();
/**
*
* @since 1.8
*/
AnnotatedType[] getAnnotatedBounds();
}
public class TestType<K extends List & Serializable, V> {
}
/**
* [java.util.List, java.io.Serializable]
* K
* class com.chris.reflect.type.bean.TestType
* -----------------
* [java.lang.Object]
* V
* class com.chris.reflect.type.bean.TestType
* -----------------
*/
@Test
public void test2() {
TypeVariable[] v = TestType.class.getTypeParameters();
for (TypeVariable t : v) {
System.out.println(Arrays.stream(t.getBounds()).map(Type::getTypeName).collect(Collectors.toList()));
System.out.println(t.getName());
System.out.println(t.getGenericDeclaration());
System.out.println("-----------------");
}
}
| 创建时间: | 2020/9/2 16:15 |
| 更新时间: | 2022/12/3 14:41 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/qusikao/article/details/126737400 |
https://www.cnblogs.com/hongdada/p/9776547.html
偏好设置->打开高级设置->conf.user.json文件
"keyBinding": {
// "Always on Top": "Ctrl+Shift+P"
"Always on Top": "Ctrl+Shift+P",
"Code Fences": "Ctrl+Shift+F",
"Ordered List":"Ctrl+Alt+o",
"Unordered List": "Ctrl+Alt+u"
},
Microsoft YaHe
font-size: 14px

#write {
max-
margin: 0 auto;
padding: 20px 30px 160px;
}
https://blog.csdn.net/qusikao/article/details/126737400
偏好设置,外观,打开主题文件夹
选择使用中的主题
将max-width的值调整为94%,意思是在全屏状态下编辑器窗口宽度会自动占用94%的宽度。
#write {
max-width: 94%;
margin: 0 auto;
padding: 20px 30px 160px;
}
对于高阶版用户可能还会调整源文件编辑器宽度,依然是调整对应主题的css文件,代码如下:
#typora-source .CodeMirror-lines{
max-width: 94%;
}
| 创建时间: | 2021/12/29 15:18 |
| 更新时间: | 2022/12/3 12:56 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/IT_heima/article/details/124058108 |
基于Go语言实现的云开源项目
Docker的主要目标是:Build,Ship and Run any app, anywhere
解决了运行环境和配置问题软件容器,方便做系统集成并有助于整体发布的容器虚拟化技术
更轻量:
基于容器的虚拟化,仅包含业务运行所需要的runtime环境[root用户权限,进程空间,用户空间和网络空间]
CentOS、Ubuntu基础镜像仅170M,这宿主机可部署100~1000个容器
更高效:
一次构建,随处运行
更快速高效的交付和部署系统
降低运维成本,更便捷的升级和扩容
镜像
容器
仓库
镜像就是模板,可以用来创建Docker容器,一个镜像可以创建很多个容器
容器是用镜像创建的运行实例
集中存放镜像文件的场所
仓库分为公开仓库和私有仓库
最大的公开仓库是DockerHubhttps://hub.docker.com/
国内的公开仓库有阿里和网易
Docker是一个CS结构的系统,Docker守护进程运行在主机上,然后通过Socket连接从客户端访问,守护进程从客户端接受命令并管理运行在主机上的容器

官网:
http://www.docker.com
http://www.docker-cn.com
Docker运行在CentOS7上要求系统版本为64位,内核版本为3.10以上
Docker运行在CentOS6.5上要求系统版本为64位,内核版本为2.6.32-431或以上
[root@master ~]# uname -r
3.10.0-1127.el7.x86_64
https://docs.docker.com/install/linux/docker-ce/centos
卸载老版本
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
安装仓库
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
查看可用docker版本
yum list docker-ce --showduplicates | sort -r
安装指定版本
yum install docker-ce-19.03.8 docker-ce-cli-19.03.8 containerd.io
启动docker
systemctl start docker

https://dev.aliyun.com/search.html

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json
{
"registry-mirrors": ["https://96rbv7qx.mirror.aliyuncs.com"]
}
sudo systemctl daemon-reload
sudo systemctl restart docker
docker run hello-world
[root@master ~]# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:f2266cbfc127c960fd30e76b7c792dc23b588c0db76233517e1891a4e357d519
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
| 描述 | 命令 |
|---|---|
| 查看版本 | docker version |
| docker信息汇总 | docker info |
| 帮助命令 | docker --help |
| 描述 | 命令 |
|---|---|
| 列出本地镜像 | docker images |
| 列出本地所有镜像包括是间映像层 | docker image -a |
| 只显示镜像ID | docker image -q |
| 显示镜像的摘要信息 | docker image --digests |
| 显示完整的镜像信息 | docker image --no-trunc |
[root@master ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest d1165f221234 6 weeks ago 13.3kB
REPOSITORY:镜像仓库源
TAG:镜像的标签
IMAGE ID:镜像ID
CREATED:镜像创建时间
SIZE:镜像大小
如查不你指定一个镜像的标签,docker默认使用hello-world:latest镜像

STARS:点赞数
OFFICIAL:官方版本
| 描述 | 命令 |
|---|---|
| 从[https://hub.docker.com]上查询镜像信息 | docker search image-name |
| 列出收藏数不小于30的镜像 | docker search -s 30 image-name |
| 显示完整的镜像信息 | docker search --no-trunc image-name |
| 只列出automated的镜像 | docker search --automated image-name |
| 描述 | 命令 |
|---|---|
| 摘取远程镜像到本地,不加版本默认拉取最新版本 | docker pull elasticsearch |
| 描述 | 命令 |
|---|---|
| 删除本地镜像,不加版本默认删除最新版本 | docker rmi elasticsearch |
| 删除多个本地镜像,不加版本默认删除最新版本 | docker rmi elasticsearch nginx |
| 强制删除本地镜像,不加版本默认删除最新版本 | docker rmi -f elasticsearch |
| 强制删除本地所有镜像,不加版本默认删除最新版本 | docker rmi -f $(docker images -q) |
| 描述 | 命令 |
|---|---|
| 提交容器副本使之成为一个新的镜像 | docker commit |
docker commit -m="信息描述" -a="作者" 容器ID 要创建的目标镜像:[标签名]
[root@master ~]# docker commit -m "tomcat without doc" -a "chris" 3b7146c6590e christomcat:1.0
[root@master ~]# docker run -it -p 8888:8080 christomcat:1.0
| 描述 | 命令 |
|---|---|
| 新建并启动容器 | docker run [OPTIONS] (image-name|id) [COMMAND] [ARG...] |
| --name="指定新的容器名称" | docker run --name 指定新的容器名称 (image-name|id) |
| 以交互方式运行容器通常与-t并用 | docker run -i (image-name|id) |
| 为容器重新分配一个伪输入终端 | docker run -t (image-name|id) |
| 随机端口映射 | docker run -P (image-name |
| 指定端口映射,有四种形式 | docker run -p (image-name|id) |
| 1 | docker run -p ip:hostPort:containPort (image-name|id) |
| 2 | docker run -p ip::containPort (image-name|id) |
| 3 | docker run -p hostPort:containPort (image-name|id) |
| 4 | docker run -p containPort (image-name|id) |
| 以守护的方启动容器 | docker run -d (image-name|id) |
docker run -it --name="es0422" elasticsearch /bin/bash
docker run -d centos
docker run -it -p 8080:8080 tomcat /bin/bash

| 描述 | 命令 |
|---|---|
| 所有正在运行的容器 | docker ps [OPTIONS] |
| 列出正在运行的和历史上运行过的容器 | docker ps -a |
| 列出最近创建的容器 | docker ps -l |
| 列出最近3个创建的容器 | docker ps -n 3 |
| 静默模式,只显示容器编号 | docker ps -q |
| 不截断输出 | docker ps --no-trunc |
| 描述 | 命令 |
|---|---|
| 停止容器并退出 | exit |
| 退出但不停止容器 | ctrl+p+q |
| 描述 | 命令 |
|---|---|
| 启动容器 | docker start (container-name|id) |
| 重新启动容器 | docker restart (container-name|id) |
| 停止容器 | docker stop (container-name|id) |
| 强制停止容器 | docker kill (container-name|id) |
| 描述 | 命令 |
|---|---|
| 删除已停止的容器 | docker rm (container-name|id) |
| 强制删除容器无论容器是否已停止 | docker rm (container-name|id) |
| 一次性删除多个容器 | docker rm -f $(docker ps -aq) |
| 一次性删除多个容器 | docker ps -aq | xargs docker rm |
docker run -d centos /bin/sh -c "while true; do echo hello zzyy;sleep 2;done"
[root@master docker]# docker run -d centos /bin/sh -c "while true; do echo hello zzyy;sleep 2;done"
9b5688f06fc2c4a52721e6dbcbba94062915211bbfaceb9473b8d3bfced736ce
[root@master ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9b5688f06fc2 centos "/bin/sh -c 'while t…" 5 seconds ago Up 2 seconds wonderful_goldwasser
docker logs -f -t --tail 3 9b5688f06fc2
-f: 打印最新的日志
-t: 加入时间戳
--tail: 显示最后多少条
[root@master ~]# docker logs -f -t --tail 3 9b5688f06fc2
2021-04-22T13:39:23.793299808Z hello zzyy
2021-04-22T13:39:25.804149907Z hello zzyy
2021-04-22T13:39:27.815085938Z hello zzyy
2021-04-22T13:39:29.826784950Z hello zzyy
| 描述 | 命令 |
|---|---|
| 查看容器内的进程 | docker -ps |
| 查看容器内的细节 | docker inspect id |
| 描述 | 命令 |
|---|---|
| 进入容器正在执行的终端 | docker attach id |
| 进入容器内 | docker exec -it id /bin/bash |
| 在容器外操作执行命令 | docker exec -it id ls -l /tmp |
- 进入当前容器后开启一个新的终端,可以在里面操作。(常用)
docker exec- 进入容器正在执行某个命令的终端,不能在里面操作;多个窗口同时attach到同一个容器时,所有窗口同步显示;当某个窗口因命令阻塞,其他窗口也无法操作
docker attach
[root@master docker]# docker attach 013c88f998bd
[root@013c88f998bd /]# ls -l /tmp
total 8
-rwx------. 1 root root 701 Dec 4 17:37 ks-script-esd4my7v
-rwx------. 1 root root 671 Dec 4 17:37 ks-script-eusq_sc5
[root@master ~]# docker exec 013c88f998bd ls -l /tmp
total 8
-rwx------. 1 root root 701 Dec 4 17:37 ks-script-esd4my7v
-rwx------. 1 root root 671 Dec 4 17:37 ks-script-eusq_sc5
容器停止后容器里面之前改动的数据就会消失
docker cp id:/容器内路径 宿主机路径
docker cp id:/tmp/yum.log /root
联合文件系统(UnionFS), 是一种分层,轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时将不同的目录挂载到同一个虚拟文件下
bootfs(boot file system):主要包含bootloader和kernel,bootloader主要是引导加载kernel,linux启动时会先加载bootfs,在Docker镜像的最底层就是bootfs,这一层与linux是一样的,当bootfs加载完成之后整个内核就都在内存中,此时内存的使用权就由bootfs转交给内核,同时系统也会卸载掉bootfs。
rootfs(root file system):在bootfs之上包含了典型的linux系统中的/dev,/proc,/bin,/etc 等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu或CentOS
为什么我们安装的CentOS要好几个G,但是docker里面的CentOS却只有200M
对于不同的linux发行版本bootfs是一致的,rootfs会有差别,对于一个精简的OS,可以共用系统的bootfs并提供自己的rootfs
tomcat镜像里面的分层结构
Docker镜像都是只读的
当容器启动时,当容器启动时当一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下叫作“镜像层”
docker容器产生的数据,如果不通过docker commit生成新的镜像,当容器删除之后,数据就会消失
容器数据卷就是用来保存docker容器中的数据
卷是目录或文件,存在于一个或多个容器中,由docker挂载到容器,但是不属于UnionFS,因此能绕过UnionFS提供一些用于持久存储或共享数据的特性
卷的设计目的就是数据持久化,完全独立于容器的生命周期,因此docker不会在容器删除时删除挂载的数据卷
1. 数据卷可以在容器之间共享或重用数据
2. 数据卷中的更改可以直接生效
3. 数据卷中的更改不会包含在镜像的更新中
4. 数据卷的生命周期一直持续到没有容器使用它为止
docker run -it -v /宿主机目录绝对路径:容器内目录[权限:ro] 镜像名或ID
docker run -it -v /mydataVolume":/dataVolumeContainer: centos
在目录不存在的情况会自动创建目录, 如果创建添加权限:ro, 表示容器里面的数据卷只读但不能修改
# docker inspect 250d8813d860
"HostConfig": {
"Binds": [
"/mydataVolume:/dataVolumeContainer:"
],
}
"Mounts": [
{
"Type": "bind",
"Source": "/mydataVolume",
"Destination": "/dataVolumeContainer",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
[root@master ~]# docker run -it -v /mydataVolume:/dataVolumeContainer: centos
[root@250d8813d860 /]# ls
bin dataVolumeContainer dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@master /]# ll
total 24
lrwxrwxrwx. 1 root root 7 Dec 24 11:26 bin -> usr/bin
dr-xr-xr-x. 5 root root 4096 Dec 24 11:48 boot
drwxr-xr-x. 20 root root 3300 Apr 23 15:29 dev
drwxr-xr-x. 141 root root 8192 Apr 23 15:29 etc
drwxr-xr-x. 3 root root 19 Dec 24 11:53 home
lrwxrwxrwx. 1 root root 7 Dec 24 11:26 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Dec 24 11:26 lib64 -> usr/lib64
drwxr-xr-x. 2 root root 6 Apr 11 2018 media
drwxr-xr-x. 2 root root 6 Apr 11 2018 mnt
drwxr-xr-x. 2 root root 6 Apr 25 10:15 mydataVolume
drwxr-xr-x. 4 root root 34 Apr 22 13:49 opt
dr-xr-xr-x. 256 root root 0 Apr 23 15:28 proc
dr-xr-x---. 6 root root 4096 Apr 23 15:29 root
drwxr-xr-x. 43 root root 1340 Apr 25 06:35 run
lrwxrwxrwx. 1 root root 8 Dec 24 11:26 sbin -> usr/sbin
drwxr-xr-x. 2 root root 6 Apr 11 2018 srv
dr-xr-xr-x. 13 root root 0 Apr 23 15:28 sys
drwxrwxrwt. 22 root root 4096 Apr 25 10:15 tmp
drwxr-xr-x. 13 root root 155 Dec 24 11:26 usr
drwxr-xr-x. 20 root root 282 Dec 24 11:50 var
备注
Docker挂载主机目录Docker访问出现 cann't open directory.:Permission denied
解决办法在挂载目录后增加--privileged true参数即可
[root@master ~]# docker run -it -v /mydataVolume:/dataVolumeContainer: --privileged true centos
mkdir mydocker
cd mydocker
vi dockerfile
写入如下命令
From centos
VOLUME ["/dataVolumeContainer01","/dataVolumeContainer02"]
CMD echo "finished,------success"
CMD /bin/bash
docker build -f /mydocker/dockerfile -t chris/centos .
在当前目录下根据指定的dockerfile生成命名空间为chris镜像名称为centos的镜像
[root@master mydocker]# docker build -f /mydocker/dockerfile -t chris/centos .
Sending build context to Docker daemon 2.048kB
Step 1/4 : From centos
---> 300e315adb2f
Step 2/4 : VOLUME ["/dataVolumeContainer01","/dataVolumeContainer02"]
---> Running in 3a2ba255152a
Removing intermediate container 3a2ba255152a
---> 9522ef03e277
Step 3/4 : CMD echo "finished,------success"
---> Running in b6c0a82df0ad
Removing intermediate container b6c0a82df0ad
---> 7dff01de54a6
Step 4/4 : CMD /bin/bash
---> Running in af67b3613d42
Removing intermediate container af67b3613d42
---> f975da2854b7
Successfully built f975da2854b7
Successfully tagged chris/centos:latest
[root@master /]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chris/centos latest f975da2854b7 3 minutes ago 209MB
[root@master ~]# docker run -it chris/centos /bin/bash
[root@22f74a27ccb0 /]# ls -l
total 0
lrwxrwxrwx. 1 root root 7 Nov 3 15:22 bin -> usr/bin
drwxr-xr-x. 2 root root 6 Apr 25 03:20 dataVolumeContainer01
drwxr-xr-x. 2 root root 6 Apr 25 03:20 dataVolumeContainer02
drwxr-xr-x. 5 root root 360 Apr 25 03:20 dev
drwxr-xr-x. 1 root root 66 Apr 25 03:20 etc
drwxr-xr-x. 2 root root 6 Nov 3 15:22 home
lrwxrwxrwx. 1 root root 7 Nov 3 15:22 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Nov 3 15:22 lib64 -> usr/lib64
drwx------. 2 root root 6 Dec 4 17:37 lost+found
drwxr-xr-x. 2 root root 6 Nov 3 15:22 media
drwxr-xr-x. 2 root root 6 Nov 3 15:22 mnt
drwxr-xr-x. 2 root root 6 Nov 3 15:22 opt
dr-xr-xr-x. 258 root root 0 Apr 25 03:20 proc
dr-xr-x---. 2 root root 162 Dec 4 17:37 root
drwxr-xr-x. 11 root root 163 Dec 4 17:37 run
lrwxrwxrwx. 1 root root 8 Nov 3 15:22 sbin -> usr/sbin
drwxr-xr-x. 2 root root 6 Nov 3 15:22 srv
dr-xr-xr-x. 13 root root 0 Apr 23 07:28 sys
drwxrwxrwt. 7 root root 145 Dec 4 17:37 tmp
drwxr-xr-x. 12 root root 144 Dec 4 17:37 usr
drwxr-xr-x. 20 root root 262 Dec 4 17:37 var
在数据卷中创建文件
[root@22f74a27ccb0 /]# cd dataVolumeContainer01
[root@22f74a27ccb0 dataVolumeContainer01]# vi container.txt
查看数据卷在容器和宿主机之间的路径关联关系
[root@master ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
22f74a27ccb0 chris/centos "/bin/bash" 16 minutes ago Up 16 minutes stupefied_cannon
692aee0e4bfe christomcat:1.0 "catalina.sh run" 42 hours ago Up 42 hours 0.0.0.0:8888->8080/tcp epic_roentgen
3b7146c6590e tomcat "catalina.sh run" 43 hours ago Up 43 hours 0.0.0.0:8088->8080/tcp tomcat2
[root@master ~]# docker inspect 22f74a27ccb0
"Mounts": [
{
"Type": "volume",
"Name": "f893623ee2039a23c820361dda8b45a164dc9a6a3e4329d30533ea6a73ad36e9",
"Source": "/var/lib/docker/volumes/f893623ee2039a23c820361dda8b45a164dc9a6a3e4329d30533ea6a73ad36e9/_data",
"Destination": "/dataVolumeContainer01",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
{
"Type": "volume",
"Name": "feb81fb00529025525f7b27c20edc417eced5ce517cdf73c754343e943af50ba",
"Source": "/var/lib/docker/volumes/feb81fb00529025525f7b27c20edc417eced5ce517cdf73c754343e943af50ba/_data",
"Destination": "/dataVolumeContainer02",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
]
[root@master ~]# cd /var/lib/docker/volumes/f893623ee2039a23c820361dda8b45a164dc9a6a3e4329d30533ea6a73ad36e9/_data
[root@master _data]# ll
total 4
-rw-r--r--. 1 root root 29 Apr 25 11:36 container.txt
命名的容器挂载数据卷,其它容器通过挂载这个父容器实现数据共享,挂载数据卷的容器称之为数据卷容器
容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用它为止
[root@master _data]# docker run -it --name dc01 chris/centos
[root@ca18f7f9d24c /]# cd dataVolumeContainer02
[root@ca18f7f9d24c dataVolumeContainer02]# touch dc01_add.txt
[root@master ~]# docker run -it --volumes-from dc01 --name dc02 chris/centos
[root@defe4e49519f /]# ls
bin dataVolumeContainer01 dataVolumeContainer02 dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@defe4e49519f /]# cd dataVolumeContainer0
[root@defe4e49519f dataVolumeContainer02]# ls
dc01_add.txt
DockerFile是用来构建Docker镜像的构建文件,是由一系列的命令和参数组成的脚本。
- 每个保留字命令必须为大写字母并且后面至少跟一个参数
- 指令按从上到下顺序执行
- #表示注释
- 每条指令都会创建一个新的镜像层,并对镜像进行提交
- docker从基础镜像运行一个容器
- 执行一条指定并对容器作出修改
- 执行类似docker commit 的操作提交一个新的镜像层
- docker基于则提交的镜像再运行一个新的容器
- 执行DockerFile中的下一条指令,直到所有的指令都被执行完成。
DockerFile是软件的原材料
Docker镜像是软件的交付物
Docker容器可以看成是软件的运行态
DockerFile面向开发,Docker镜像成为软件的交付标准,Docker容器则涉及部署到运维,三者缺一不可
| 命令 | 描述 |
|---|---|
| FROM | 基础镜像,当前镜像是基于哪个镜像的 |
| MAINTAINER | 作者及其邮箱 |
| RUN | 容器构建时需要运行的linux命令 |
| EXPOSE | 容器运行时对外暴露的端口号 |
| WORKDIR | 进入容器运行后的目录,如果没有指定就是根目录 |
| ENV | 在构建镜像的过程中设置环境变量 |
| ADD | 将宿主机的文件拷贝进镜像且ADD命令会自动处理URL和解压TAR压缩包 |
| COPY | 类似ADD,将文件或目录拷贝到镜像中,将从构建上下文目录中<源路径>的文件/目录复制到新的一层镜像内的<目标路径>位置 COPY SRC DEST |
| VOLUME | 指定数据卷,用于数据共享和持久化 |
| CMD | 指定一个容器启动时需要运行的命令,DockerFile中可以有多个CMD指令,但是只有最后一个生效,CMD会被docker run之后的参数替换 |
| ENTRYPOINT | 和CMD一样,指定一个容器启动时需要运行的命令,但是ENTRYPOINT只会追加,不会覆盖之前的命令 |
| ONBUILD | 当构建一个被继承的DockerFile时运行命令,父镜像在被子继承后父镜像的ONBUILD被触发 |
DockerHub中有99%的镜像都是通过在Base镜像中安装或配置需要软件构建出来的
cd /mydocker
vi dockerfile_mycentos
FROM centos
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN yum -y install vim
RUN yum -y install net-tools
EXPOSE 80
CMD echo $MYPATH
CMD echo "success -------"
CMD /bin/bash
[root@master mydocker]# docker build -f dockerfile_mycentos -t chris/mycentos .
Sending build context to Docker daemon 2.048kB
Step 1/9 : FROM centos
---> 300e315adb2f
Step 2/9 : ENV MYPATH /usr/local
---> Running in 2949185ee980
Removing intermediate container 2949185ee980
---> 0f46cb36876f
Step 3/9 : WORKDIR $MYPATH
---> Running in fe888cab4aa9
Removing intermediate container fe888cab4aa9
---> 64d820bebb93
Step 4/9 : RUN yum -y install vim
---> Running in 96061faccf00
......
---> be8b79c33752
Step 6/9 : EXPOSE 80
---> Running in b2d74036a236
Removing intermediate container b2d74036a236
---> b7b067e37daa
Step 7/9 : CMD echo $MYPATH
---> Running in 1532b91d6170
Removing intermediate container 1532b91d6170
---> d4370aa3b2ff
Step 8/9 : CMD echo "success -------ok"
---> Running in 0fe95c30e852
Removing intermediate container 0fe95c30e852
---> 6c3800d76c3c
Step 9/9 : CMD /bin/bash
---> Running in 3a47cc176453
Removing intermediate container 3a47cc176453
---> eee12504217a
Successfully built eee12504217a
Successfully tagged chris/mycentos:latest
[root@master mydocker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chris/mycentos latest eee12504217a 2 minutes ago 291MB
tomcat latest c0e850d7b9bb 8 days ago 667MB
hello-world latest d1165f221234 8 weeks ago 13.3kB
centos latest 300e315adb2f 4 months ago 209MB
[root@master mydocker]# docker run -it --name centos02 chris/mycentos
[root@960bafdfce5e local]# pwd
/usr/local
docker history 镜像名|ID
从下往上查看镜像的生成历史
[root@master chris]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chris/mycentos latest eee12504217a 15 minutes ago 291MB
tomcat latest c0e850d7b9bb 8 days ago 667MB
hello-world latest d1165f221234 8 weeks ago 13.3kB
centos latest 300e315adb2f 4 months ago 209MB
[root@master chris]# docker history chris/mycentos
IMAGE CREATED CREATED BY SIZE COMMENT
eee12504217a 15 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "/bin… 0B
6c3800d76c3c 15 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "echo… 0B
d4370aa3b2ff 15 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "echo… 0B
b7b067e37daa 15 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
be8b79c33752 15 minutes ago /bin/sh -c yum -y install net-tools 23.4MB
7b1c6d927962 16 minutes ago /bin/sh -c yum -y install vim 58.1MB
64d820bebb93 16 minutes ago /bin/sh -c #(nop) WORKDIR /usr/local 0B
0f46cb36876f 16 minutes ago /bin/sh -c #(nop) ENV MYPATH=/usr/local 0B
300e315adb2f 4 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 months ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 4 months ago /bin/sh -c #(nop) ADD file:bd7a2aed6ede423b7… 209MB
[root@master chris]#
CMD 指的是下列表里面的COMMAND
[root@master chris]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960bafdfce5e chris/mycentos "/bin/sh -c /bin/bash" 15 minutes ago Up 15 minutes 80/tcp centos02
8034e792d331 centos "/bin/bash" 26 minutes ago Up 26 minutes centos01
[root@master chris]# docker run -it --name tomcat02 -p 8888:8080 tomcat
[root@master chris]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b0b3c209535 tomcat "catalina.sh run" 5 seconds ago Up 4 seconds 0.0.0.0:8888->8080/tcp tomcat02
在后面加 ls -l命令后,新增的tomcat03容器并没有启动
[root@master chris]# docker run -it --name tomcat03 -p 8088:8080 tomcat ls -l
total 128
-rw-r--r--. 1 root root 18984 Mar 30 10:29 BUILDING.txt
-rw-r--r--. 1 root root 5587 Mar 30 10:29 CONTRIBUTING.md
-rw-r--r--. 1 root root 57092 Mar 30 10:29 LICENSE
-rw-r--r--. 1 root root 2333 Mar 30 10:29 NOTICE
-rw-r--r--. 1 root root 3257 Mar 30 10:29 README.md
-rw-r--r--. 1 root root 6898 Mar 30 10:29 RELEASE-NOTES
-rw-r--r--. 1 root root 16507 Mar 30 10:29 RUNNING.txt
drwxr-xr-x. 2 root root 4096 Apr 22 23:10 bin
drwxr-xr-x. 2 root root 238 Mar 30 10:29 conf
drwxr-xr-x. 2 root root 4096 Apr 22 23:09 lib
drwxrwxrwx. 2 root root 6 Mar 30 10:29 logs
drwxr-xr-x. 2 root root 134 Apr 22 23:10 native-jni-lib
drwxrwxrwx. 2 root root 30 Apr 22 23:09 temp
drwxr-xr-x. 2 root root 6 Apr 22 23:09 webapps
drwxr-xr-x. 7 root root 81 Mar 30 10:29 webapps.dist
drwxrwxrwx. 2 root root 6 Mar 30 10:29 work
[root@master chris]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
81cbfbd01279 tomcat "ls -l" 6 seconds ago Exited (0) 5 seconds ago tomcat03
原因:tomcat dockerfile 最后一行的CMD命令被 docker run 后面的 ls -l 替换掉
EXPOSE 8080
CMD ["catalina.sh", "run"]
[root@master]# cd /mydocker
[root@master]# vi myip01
FROM centos
RUN yum -y install curl
CMD ["curl","-s","http://www.ip.cn"]
[root@master mydocker]# docker build -f myip01 -t myip01:1.0 .
[root@master mydocker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myip01 1.0 0ea4cc5030b2 7 seconds ago 243MB
[root@master]# cd /mydocker
[root@master]# vi myip02
FROM centos
RUN yum -y install curl
ENTRYPOINT ["curl","-s","http://www.ip.cn"]
[root@master mydocker]# docker build -f myip02 -t myip02:1.0 .
Sending build context to Docker daemon 4.096kB
Step 1/3 : FROM centos
---> 300e315adb2f
Step 2/3 : RUN yum -y install curl
---> Using cache
---> 552703ee39c9
Step 3/3 : ENTRYPOINT ["curl","-s","http://www.ip.cn"]
---> Running in 0dbc34763358
Removing intermediate container 0dbc34763358
---> a8f738d4a45a
Successfully built a8f738d4a45a
Successfully tagged myip02:1.0
[root@master]# cd /mydocker
[root@master mydocker]# vi myip02_father
FROM centos
RUN yum -y install curl
ENTRYPOINT ["curl","-s","cip.cc"]
ONBUILD RUN echo "myip02_father image onbuild-----ok"
[root@master mydocker]# docker build -f myip02_father -t myip02_father:1.0 .
Sending build context to Docker daemon 5.12kB
Step 1/3 : FROM centos
---> 300e315adb2f
Step 2/3 : RUN yum -y install curl
---> Using cache
---> 552703ee39c9
Step 3/3 : ENTRYPOINT ["curl","-s","http://www.ip.cn"]
---> Using cache
---> a8f738d4a45a
Successfully built a8f738d4a45a
Successfully tagged myip02_father:1.0
[root@master mydocker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myip02_father 1.0 a8f738d4a45a 3 minutes ago 243MB
[root@master]# cd /mydocker
[root@master mydocker]# vi myip02_kid
FROM myip02_father:1.0
RUN yum -y install curl
ENTRYPOINT ["curl","-s","cip.cc"]
[root@master mydocker]# docker build -f myip02_kid -t myip02_kid:1.0 .
Sending build context to Docker daemon 6.144kB
Step 1/3 : FROM myip02_father:1.0
# Executing 1 build trigger
---> Running in 61b5fd1fece7
myip02_father image onbuild-----ok
Removing intermediate container 61b5fd1fece7
---> 4d14bc2d40e8
Step 2/3 : RUN yum -y install curl
---> Running in e328e5961d4f
Last metadata expiration check: 0:08:57 ago on Sun May 2 01:54:21 2021.
Package curl-7.61.1-14.el8_3.1.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
Removing intermediate container e328e5961d4f
---> e44a48f55d44
Step 3/3 : ENTRYPOINT ["curl","-s","cip.cc"]
---> Running in d5e81a13e501
Removing intermediate container d5e81a13e501
---> df8fa730dfe4
Successfully built df8fa730dfe4
Successfully tagged myip02_kid:1.0
mkdir -p /mydocker/christomcat9
[root@master]# cd /mydocker/christomcat9
[root@master chrisTomcat9]# touch c.txt
[root@master chrisTomcat9]# cp /opt/jdk-8u191-linux-x64.tar.gz .
[root@master chrisTomcat9]# cp /opt/apache-tomcat-9.0.38.tar.gz .
[root@master chrisTomcat9]# touch Dockerfile
FROM centos
MAINTAINER chris<lilunlogic@163.com>
#把宿主机当前上下文的c.txt拷贝到容器/usr/local/路径下
COPY c.txt /usr/local/cincontainer.txt
#把java与tomcat添加到容器中
ADD jdk-8u191-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.38.tar.gz /usr/local/
#安装vim编辑器
RUN yum -y install vim
#设置工作访问时候的WORKDIR路径,登录落脚点
ENV MYPATH /usr/local
WORKDIR $MYPATH
#配置java与tomcat环境变量
ENV JAVA_HOME /usr/local/jdk1.8.0_191
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.38
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.38
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#容器运行时监听的端口
EXPOSE 8080
#启动时运行tomcat
# ENTRYPOINT ["/usr/local/apache-tomcat-9.0.38/bin/startup.sh" ]
# CMD ["/usr/local/apache-tomcat-9.0.38/bin/catalina.sh","run"]
CMD /usr/local/apache-tomcat-9.0.38/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.38/bin/logs/catalina.out
[root@master chrisTomcat9]# docker build -t christomcat9 .
[root@master opt]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
christomcat9 latest a02261dc7206 8 minutes ago 680MB
[root@master]# docker run -d -p 9080:8080 --name christomcat9 -v /mydocker/tomcat9/test:/usr/local/apache-tomcat-9.0.38/webapps/test -v /mydocker/tomcat9/tomcat9logs/:/usr/local/apache-tomcat-9.0.38/logs --privileged=true christomcat9
[root@master]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b2d9623ad57d christomcat9 "/bin/sh -c '/usr/lo…" 7 seconds ago Up 6 seconds 0.0.0.0:9080->8080/tcp christomcat9
[root@master]# docker exec -it christomcat9 java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
https://cr.console.aliyun.com/cn-hangzhou/instance/repositories
命名空间:chris0716
仓库名称:mydocker
$ sudo docker login --username=lilunlogic@163.com registry.cn-hangzhou.aliyuncs.com
$ pwd:Lilun215+
$ sudo docker tag [ImageId] registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:[阿里云上显示的镜像版本号]
$ sudo docker push registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:[阿里云上显示的镜像版本号]
[root@master chris]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
christomcat9 latest de6f4c6c9bcb 17 hours ago 680MB
registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker 1.0 de6f4c6c9bcb 17 hours ago 680MB
[root@master chris]# docker rmi registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:1.0
Untagged: registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:1.0
Untagged: registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker@sha256:9ed6c267619595e22642c02a585bc75600d685cd7d93b4ea5c46c6292b30efd3
[root@master chris]# docker pull registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:1.0
1.0: Pulling from chris0716/mydocker
Digest: sha256:9ed6c267619595e22642c02a585bc75600d685cd7d93b4ea5c46c6292b30efd3
Status: Downloaded newer image for registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:1.0
registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker:1.0
[root@master chris]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
christomcat9 latest de6f4c6c9bcb 17 hours ago 680MB
registry.cn-hangzhou.aliyuncs.com/chris0716/mydocker 1.0 de6f4c6c9bcb 17 hours ago 680MB
| 创建时间: | 2022/11/30 22:53 |
| 更新时间: | 2022/11/30 23:09 |
| 作者: | Chris |
位运算分为两类 :
只有当两个位都是1的时候结果是1否则结果是0

只有当两个位都是0的时候结果才是0,否则结果为1

只有当两个位不同的时候结果才是1.否则结果为0

是一个一元运算符,就是把当前数转为二进制后0变1,1变0

如果i是正数则 i / 2的n次方并向下取整
@Test
public void leftShift() {
int i = 3;
System.out.println(i << 1);
System.out.println(i << 2);
System.out.println(i << 3);
}
i x 2的n次方
@Test
public void rightShift() {
int i = -16;
System.out.println(i >> 1);
System.out.println(i >> 2);
System.out.println(i >> 3);
System.out.println(i >> 4);
System.out.println(i >> 5);
}
| 创建时间: | 2022/3/4 20:00 |
| 更新时间: | 2022/11/29 0:03 |
| 作者: | Chris |
[root@master chris]# uname -a
Linux master 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
https://downloads.mysql.com/archives/community/

下载后的压缩文件为
mysql-8.0.27-1.el7.x86_64.rpm-bundle.tar
tar xvf mysql-8.0.27-1.el7.x86_64.rpm-bundle.tar
mysql-community-client-8.0.27-1.el7.x86_64.rpm
mysql-community-client-plugins-8.0.27-1.el7.x86_64.rpm
mysql-community-common-8.0.27-1.el7.x86_64.rpm
mysql-community-devel-8.0.27-1.el7.x86_64.rpm
mysql-community-embedded-compat-8.0.27-1.el7.x86_64.rpm
mysql-community-libs-8.0.27-1.el7.x86_64.rpm
mysql-community-libs-compat-8.0.27-1.el7.x86_64.rpm
mysql-community-server-8.0.27-1.el7.x86_64.rpm
mysql-community-test-8.0.27-1.el7.x86_64.rpm
只需要安装下面这几个文件
mysql-community-client-8.0.27-1.el7.x86_64.rpm
mysql-community-client-plugins-8.0.27-1.el7.x86_64.rpm
mysql-community-common-8.0.27-1.el7.x86_64.rpm
mysql-community-libs-8.0.27-1.el7.x86_64.rpm
mysql-community-server-8.0.27-1.el7.x86_64.rpm
执行命令
[root@localhost /]# rpm -qa | grep mysql
从执行结果,可以看出我们已经安装了mysql-libs-5.1.73-5.el6_6.x86_64,执行删除命令
[root@localhost /]# rpm -e --nodeps mysql-libs-5.1.73-5.el6_6.x86_64
由于mysql安装过程中会通过/tmp目录创建临时文件tmp_db,所以需要给/tmp赋予较大权限
chmod -R 777 /tmp
[root@master /]# rpm -qa | grep libaio
libaio-0.3.109-13.el7.x86_64
[root@master /]# rpm -qa | grep net-tools
net-tools-2.0-0.25.20131004git.el7.x86_64
检查mysql用户组和用户是否存在,如果没有,则创建
[root@localhost /]# cat /etc/group | grep mysql
[root@localhost /]# cat /etc/passwd |grep mysql
[root@localhost /]# groupadd mysql
[root@localhost /]# useradd -m -d /home/mysql -s /bin/bash -g mysql mysql
安装过程中必须严格按照如下顺序进行
rpm -ivh mysql-community-common-8.0.27-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-plugins-8.0.27-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-8.0.27-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-8.0.27-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-8.0.27-1.el7.x86_64.rpm
[root@master mysql]# rpm -ivh mysql-community-common-8.0.27-1.el7.x86_64.rpm
warning: mysql-community-common-8.0.27-1.el7.x86_64.rpm: Header V3 DSA/SHA256 Signature, key ID 5072e1f5: NOKEY
Preparing... ################################# [100%]
Updating / installing...
1:mysql-community-common-8.0.27-1.e################################# [100%]
[root@master mysql]# rpm -ivh mysql-community-client-plugins-8.0.27-1.el7.x86_64.rpm
warning: mysql-community-client-plugins-8.0.27-1.el7.x86_64.rpm: Header V3 DSA/SHA256 Signature, key ID 5072e1f5: NOKEY
Preparing... ################################# [100%]
Updating / installing...
1:mysql-community-client-plugins-8.################################# [100%]
[root@master mysql]# rpm -ivh mysql-community-libs-8.0.27-1.el7.x86_64.rpm
warning: mysql-community-libs-8.0.27-1.el7.x86_64.rpm: Header V3 DSA/SHA256 Signature, key ID 5072e1f5: NOKEY
Preparing... ################################# [100%]
Updating / installing...
1:mysql-community-libs-8.0.27-1.el7################################# [100%]
[root@master mysql]# rpm -ivh mysql-community-client-8.0.27-1.el7.x86_64.rpm
warning: mysql-community-client-8.0.27-1.el7.x86_64.rpm: Header V3 DSA/SHA256 Signature, key ID 5072e1f5: NOKEY
Preparing... ################################# [100%]
Updating / installing...
1:mysql-community-client-8.0.27-1.e################################# [100%]
[root@master mysql]# rpm -ivh mysql-community-server-8.0.27-1.el7.x86_64.rpm
warning: mysql-community-server-8.0.27-1.el7.x86_64.rpm: Header V3 DSA/SHA256 Signature, key ID 5072e1f5: NOKEY
Preparing... ################################# [100%]
Updating / installing...
1:mysql-community-server-8.0.27-1.e################################# [100%]
[root@master mysql]# mysql --version
mysql Ver 8.0.27 for Linux on x86_64 (MySQL Community Server - GPL)
[root@master mysql]# rpm -qa | grep -i mysql
mysql-community-server-8.0.27-1.el7.x86_64
mysql-community-client-plugins-8.0.27-1.el7.x86_64
mysql-community-libs-8.0.27-1.el7.x86_64
mysql-community-client-8.0.27-1.el7.x86_64
mysql-community-common-8.0.27-1.el7.x86_64
[root@master mysql]# systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Active: inactive (dead)
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
[root@master mysql]# systemctl start mysqld
[root@master mysql]# systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Active: active (running) since Fri 2022-03-04 21:18:55 CST; 4s ago
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
Process: 3374 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
Main PID: 3457 (mysqld)
Status: "Server is operational"
Tasks: 38
CGroup: /system.slice/mysqld.service
└─3457 /usr/sbin/mysqld
Mar 04 21:18:48 master systemd[1]: Starting MySQL Server...
Mar 04 21:18:55 master systemd[1]: Started MySQL Server.
问题1.

解决办法:
yum remove mysql-libs 清除之前安装过的依赖即可,清除过程中遇到是否继续选择 y
[root@master mysql]# systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Active: inactive (dead)
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
[root@master mysql]# systemctl start mysqld
[root@master mysql]# systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Active: active (running) since Fri 2022-03-04 21:18:55 CST; 4s ago
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
Process: 3374 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
Main PID: 3457 (mysqld)
Status: "Server is operational"
Tasks: 38
CGroup: /system.slice/mysqld.service
└─3457 /usr/sbin/mysqld
Mar 04 21:18:48 master systemd[1]: Starting MySQL Server...
Mar 04 21:18:55 master systemd[1]: Started MySQL Server.
[root@master system]# pwd
/usr/lib/systemd/system
[root@master system]# ll | grep mysql
-rw-r--r--. 1 root root 2034 Sep 28 22:07 mysqld.service
-rw-r--r--. 1 root root 2065 Sep 28 22:07 mysqld@.service
[root@master mysql]# systemctl enable mysqld
[root@master mysql]# systemctl is-enabled mysqld
enabled
[root@master mysql]# systemctl list-units | grep mysql
mysqld.service loaded active running MySQL Server
[root@master mysql]# systemctl list-unit-files | grep mysql
mysqld.service enabled
mysqld@.service disabled
为了保证目录与文件的所有者为mysql登录用户,如果你是以root身份运行mysql服务需要执行下面命令进行初始化
mysqld --initialize --user=mysql
--initialize默认是以安全模式来初始化,则会为root用户生成一个密码标记为过期,登录后你需要设置一个新的密码,生成的临时密码会在日志中记录一份
cat /var/log/mysqld.log
2022-04-03T01:42:48.981139Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: kghUF<wCM4U5
如果登录后不修改密码会提示
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
修改密码时遇到问题:
mysql> alter user 'root'@'localhost' identified by '65536' ;
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
解决办法:
mysql> set global validate_password.policy=0;
mysql> set global validate_password.length=5;
mysql> alter user 'root'@'localhost' identified by '65536' ;
Query OK, 0 rows affected (0.01 sec)
mysql> show variables like 'validate_password%';

mysql> quit
Bye
[root@master mysql]# mysql -uroot -p
Enter password: 此处用新密码登录
密码policy对照表

宿主机host文件
C:\Windows\System32\drivers\etc\hosts
192.168.101.127 master
问题1:

解决步骤
C:\Users\Dell>ping master
C:\Users\Dell>telnet master 3066
如果连通性有问题需要关闭宿主机的firewall
mysql> select host , user from mysql.user;
+-----------+------------------+
| host | user |
+-----------+------------------+
| localhost | mysql.infoschema |
| localhost | mysql.session |
| localhost | mysql.sys |
| localhost | root |
+-----------+------------------+
4 rows in set (0.00 sec)
mysql> update mysql.user set host='%' where user='root';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select host , user from mysql.user;
+-----------+------------------+
| host | user |
+-----------+------------------+
| % | root |
| localhost | mysql.infoschema |
| localhost | mysql.session |
| localhost | mysql.sys |
+-----------+------------------+
4 rows in set (0.00 sec)
mysql>flush privileges; //刷新系统权限表
host='%' 表示允许所有客户端访问,
也可以改为 host='192.168.101.%' 表示只允许101这个网段的客户端访问
问题2:
错误号码 2058,分析是 mysql 密码加密方法变了

解决方法:
Linux下 mysql -u root -p 登录你的 mysql 数据库,然后 执行这条SQL:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'abc123';
create user chris identified by '65536';
with grant option : 表示允许用户将自己的权限授权给其它用户
grant all privileges on schema-name.* to chris'@'%' identified by '65536';
grant all privileges on schema-name.* to chris@% identified by '65536' with grant option;
grant select,update,delete,insert privileges on schema-name.* to chris@'%' identified by '65536';
| 创建时间: | 2020/9/5 13:49 |
| 更新时间: | 2022/11/28 23:51 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/ichimoku/p/7880959.html |
create user canal identified by '65536';
with grant option : 表示允许用户将自己的权限授权给其它用户
grant all privileges on schema-name.* to `user-name`@`localhost` identified by 'password';
grant all privileges on schema-name.* to `user-name`@`localhost` identified by 'password'
with grant option;
grant select,update,delete,insert privileges on schema-name.* to `user-name`@`localhost` identified by 'password';
注意:此处的"localhost",是指该用户只能在本地登录,不能在另外一台机器上远程登录。如果想远程登录的话,将"localhost"改为"%",表示在任何一台电脑上都可以登录。也可以指定某台机器可以远程登录。
回收权限
revoke delete on schema-name.* from `user-name`@`host` ;
查看用户权限
show grants for 'yangxin'@'localhost';
select @@version;
select version();
select @@global.tx_isolation;
-- mysql 8.0 后
select@@global.transaction_isolation
1. select @@innodb_autoinc_lock_mode;
2. show variables like 'innodb_autoinc_lock_mode';
show engines

show variables like %storage_engine%

InnoDB存储引擎将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小默认为
16 KB
show variables like 'innodb_page_size';
所以当你用postman测试一个分页查询接口时,发现第一次打印耗时300 ~ 400ms,往后不停的查找下一页都是30 ~ 40ms,原因就是第一次请求接口时,读数据库的时候需要读磁盘,从磁盘加载16KB的数据到内存,往后下一页的数据都是从内存中获取,没有再读磁盘,除非在内存中的16KB的数据中找不到,才会再次读磁盘获取下一个16KB的数据到内存中。
mysql中是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为
行格式或者记录格式。
行格式有4种,分别是
Dynamic、Compact、Redundant、Compressed
默认的行格式为: Dynamic
SHOW VARIABLES LIKE 'innodb_default_row_format';

show variables like 'log_bin'
CREATE TABLE `t_dg_user_address` (
`id` bigint(64) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID,根据user_type可能是app用户或小程序用户',
`user_type` smallint(6) DEFAULT '1' COMMENT '用户类型:1-代购,2-比柚商家,3-小程序',
`country_id` bigint(20) DEFAULT '0' COMMENT '国家编号',
`province_id` bigint(20) DEFAULT '0' COMMENT '省份ID',
`city_id` bigint(20) DEFAULT '0' COMMENT '城市ID',
`county_id` bigint(20) DEFAULT '0' COMMENT '区县ID',
`district_id` bigint(20) DEFAULT '0' COMMENT '街道ID',
`receiving_name` varchar(64) DEFAULT NULL COMMENT '收货人',
`receiving_phone` varchar(64) DEFAULT NULL COMMENT '收货电话',
`receiving_address` varchar(256) DEFAULT NULL COMMENT '收货地址',
`dft` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否默认 0-否 1-是',
`record_status` char(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT 'A' COMMENT '是否已经失效:A、有效;I、无效',
`create_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`creator_id` bigint(20) DEFAULT NULL,
`update_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`updater_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `index_name` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收货地址';
只复制表结构到新表
#旧表的所有字段类型都复制到新表
CREATE TABLE 新表 LIKE 旧表 ;
#主键类型和自增方式是不会复制过去的
CREATE TABLE 新表 SELECT * FROM 旧表 WHERE 1=2;
CREATE TABLE 新表 SELECT * FROM 旧表
#假设两个表结构一样
INSERT INTO 新表 SELECT * FROM 旧表
#假设两个表结构不一样
INSERT INTO 新表(字段1,字段2,.......) SELECT 字段1,字段2,...... FROM 旧表
ALTER TABLE employee ADD COLUMN abc VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE employee DROP COLUMN abc;
这条语句会向已有的表中加入一列,这一列在表的最后一列位置。如果我们希望添加在指定的一列,可以用:
alter table 表名 add column 列名 varchar(20) not null after user1;
上面这个命令的意思是说添加addr列到user1这一列后面。如果想添加到第一列的话,可以用:
alter table 表名 add column 列名 varchar(20) not null first;
INSERT INTO table_xxx VALUES();
INSERT INTO table_xxx SELECT xxx from table_yyy;
当表A和表B的表结构一致时,直接插入即可。
insert into A select * from B;
当表结构不一致时(字段大小、类型都相同)
insert into A(col1, col2) select col1, col2 from B;
-- 查询操作人的userId
select t.user_id from ipd_user_core.t_user t where t.user_name='xxx';
-- 用上面查询出来的 user_id 替换 {0}
SET @k:=(SELECT max(prm.id) FROM t_project_role_menu prm);
INSERT INTO t_project_role_menu SELECT
(@k:= @k+1) as id,
(SELECT pt.id FROM t_project_type pt WHERE pt.`name` = 'xxx' ) AS project_type_id,
NULL as template_id,
c.CODE AS role_code,
( SELECT pm.id FROM t_project_menu pm WHERE pm.menu_name = 'xxx' ) as menu_id,
0 as del_flag,
2 as app_id,
{0} as create_id, -- need fix
CURRENT_TIMESTAMP as create_time,
{0} as update_id, -- need fix
CURRENT_TIMESTAMP as update_time,
'' as project_sec_level
FROM
(
SELECT t.`code` FROM t_project_team_template t
WHERE t.del_flag = 0 AND t.STATUS = 1 AND t.template_id = 1000
UNION
SELECT tt.`code` FROM t_team_constant tt
WHERE tt.type = 2
) c;
str 要查询的字符串
strlist 字段名 参数以”,”分隔 如 (1,2,6,8,10,22)
查询字段(strlist)中包含(str)的结果,返回结果为null或记录
SELECT FIND_IN_SET('b', 'a,b,c,d');
结果:2
因为b 在strlist集合中放在2的位置 从1开始
select FIND_IN_SET('2', '1,2'); 返回2
select FIND_IN_SET('6', '1'); 返回0 strlist中不存在str,所以返回0。
功能:将多个字符串连接成一个字符串。
语法:concat(str1, str2,...)返回结果为连接参数产生的字符串,如果有任何一个参数为null,则返回值为null。

功能:和concat()一样,将多个字符串连接成一个字符串,但是可以一次性指定分隔符~(concat_ws就是concat with separator)
语法:concat_ws(separator, str1, str2, ...)
第一个参数指定分隔符。需要注意的是分隔符不能为null,如果为null,则返回结果为null。

把分隔符指定为null,结果全部变成了null:

功能:将group by产生的同一个分组中的值连接起来,返回一个字符串结果。
语法:group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )
说明:通过使用distinct可以排除重复值;如果希望对结果中的值进行排序,可以使用order by子句;separator是一个字符串值,缺省为一个逗号。
将的id号从大到小排序,且用'_' 作为分隔符:

上面的查询中显示了以name分组的每组中所有的id。接下来我们要查询以name分组的所有组的id和score:

SELECT
t.process_instance_id,
t.task_instance_id,
t.process_title,
GROUP_CONCAT( t.use_assignee, '(', t.assignee, ')' ) AS assignee,
t.update_time,
t.del_flag
FROM
t_process_instance_task t
WHERE
t.del_flag = 0
GROUP BY
t.process_instance_id
SELECT
*
FROM
t_dg_buy_user t
WHERE
im_token IS NOT NULL
AND t.create_date >= DATE_FORMAT('2019-12-19 00:00:00', '%Y-%m-%d %H:%k:%s')
AND t.create_date <= DATE_FORMAT('2019-12-19 23:00:00', '%Y-%m-%d %H:%k:%s')
ORDER BY
t.create_date DESC;
select * from t_ms_merchant t where t.shop REGEXP '^[a-z]+ [a-z]+ [a-z]';
RANK()函数为结果集的分区中的每一行分配一个排名。
行的等级由一加上前面的等级数指定。
RANK() OVER (
PARTITION BY <expression>[{,<expression>...}]
ORDER BY <expression> [ASC|DESC], [{,<expression>...}])
在这个语法中:
与ROW_NUMBER()函数不同,RANK()函数并不总是返回连续的整数。
select t.val, rank() over(order by t.val) from rankdemo t;

第二行和第三行具有相同的关系,因此它们获得相同的等级2。
第四行具有等级4,因为RANK()功能跳过等级3。
SELECT
sales_employee,
fiscal_year,
sale,
RANK( ) OVER (
PARTITION BY fiscal_year
ORDER BY sale DESC
) sales_rank
FROM
sales;

ROW_NUMBER()是一个窗口函数或分析函数,它为从1开始应用的每一行分配一个序号
ROW_NUMBER() OVER (<partition_definition> <order_definition>
ORDER BY <expression> [ASC|DESC],[{,<expression>}...] )
使用公用表表达式(CTE)返回要删除的重复行和delete语句
WITH dups AS (
SELECT
id,
name,
ROW_NUMBER()
OVER(PARTITION BY name ORDER BY name) AS row_num
FROM rowNumberDemo)
DELETE rowNumberDemo FROM rowNumberDemo INNER JOIN dups ON rowNumberDemo.id = dups.id
WHERE dups.row_num <> 1;
函数语法 INSTR(str,substr)
instr(源字符串, 目标字符串)参数说明:str:源字符串中搜索;substr: 要搜索的子字符串。
例如:instr('apple','a'),返回的查询结果是1a出现在字符串‘apple’中的第一个索引;
公用表表达式是一个命名的临时结果集
仅在单个SQL语句(例如SELECT,INSERT,UPDATE或DELETE)的执行范围内存在.
与派生表不同,CTE可以是自引用(递归CTE),也可以在同一查询中多次引用。 此外,与派生表相比,CTE提供了更好的可读性和性能。
WITH cte_name (column_list) AS (
query
)
SELECT * FROM cte_name;
查询中的列数必须与column_list中的列数相同。
如果省略column_list,CTE将使用定义CTE的查询的列列表。
CTE的名称为customers_in_usa,定义CTE的查询返回两列:customerName和state。
因此,customers_in_usa CTE返回位于美国的所有客户。
在定义美国CTE的客户之后,我们可在SELECT语句中引用它,例如,仅查询选择位于California 的客户
WITH customers_in_usa AS
(
SELECT
customerName, state
FROM customers
WHERE country = 'USA'
)
SELECT customerName FROM customers_in_usa WHERE state = 'CA' ORDER BY customerName;
返回了在2013年前五名的销售代表。
之后,我们引用了topsales2013 CTE来获取有关销售代表的其他信息,包括名字和姓氏。
WITH topsales2013 AS (
SELECT
salesRepEmployeeNumber employeeNumber,
SUM(quantityOrdered * priceEach) sales
FROM orders
INNER JOIN orderdetails USING (orderNumber)
INNER JOIN customers USING (customerNumber)
WHERE YEAR(shippedDate) = 2013
AND status = 'Shipped'
GROUP BY salesRepEmployeeNumber
ORDER BY sales DESC
LIMIT 5)
SELECT
employeeNumber, firstName, lastName, sales
FROM
employees JOIN topsales2013 USING (employeeNumber);
在同一查询中有两个CTE。
第一个CTE(salesrep)获得职位是销售代表的员工。
第二个CTE(customer_salesrep)使用INNER JOIN子句与第一个CTE连接来获取每个销售代表负责的客户。
在使用第二个CTE之后,使用带有ORDER BY子句的简单SELECT语句来查询来自该CTE的数据。
WITH salesrep AS (
SELECT employeeNumber,
CONCAT(firstName, ' ', lastName) AS salesrepName
FROM employees
WHERE jobTitle = 'Sales Rep'
),
customer_salesrep AS (
SELECT customerName,
salesrepName
FROM customers
INNER JOIN
salesrep ON employeeNumber = salesrepEmployeeNumber
)
SELECT *
FROM customer_salesrep
ORDER BY customerName;
WITH ... SELECT ...
WITH ... UPDATE ...
WITH ... DELETE ..
SELECT ... WHERE id IN (WITH ... SELECT ...);
SELECT * FROM (WITH ... SELECT ...) AS derived_table;
JSON_UNQUOTE(val)
去掉val的引号。如果val为NULL,则返回NULL。
SELECT JSON_UNQUOTE("\"123\""); -- 123
去除json字符串的引号,将值转成string类型
SELECT
userId,
JSON_EXTRACT(loginInfo, "$.cellphone") cellphone,
JSON_EXTRACT(loginInfo, "$.wxchat") wxchat
FROM UserLogin;

SELECT
userId,
JSON_UNQUOTE(JSON_EXTRACT(loginInfo, "$.cellphone")) cellphone,
JSON_UNQUOTE(JSON_EXTRACT(loginInfo, "$.wxchat")) wxchat
FROM UserLogin;

JSON_LENGTH(json_doc[, path])
获取指定路径下的长度。如果参数为NULL,则返回NULL。
长度的计算规则:
标量的长度为1;
json array的长度为元素的个数;
json object的长度为key的个数。
SELECT JSON_LENGTH('[1, 2, {"a": 3}]'); -- 3
SELECT JSON_LENGTH('{"a": 1, "b": {"c": 30}}'); -- 2
SELECT JSON_LENGTH('{"a": 1, "b": {"c": 30}}', '$.b'); -- 1
JSON_VALID(val)
判断val是否为有效的json格式,是为1,不是为0。如果参数为NUL,则返回NULL。
SELECT JSON_VALID('{"a": 1}'); -- 1
SELECT JSON_VALID('hello'), JSON_VALID('"hello"'); -- 1
JSON_KEYS(json_doc[, path])
获取json文档在指定路径下的所有键值,返回一个json array。如果有参数为NULL或path不存在,则返回NULL。
SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}'); -- ["a", "b"]
SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}', '$.b'); -- ["c"]
JSON_TYPE(json_val)
获取json文档的具体类型。如果参数为NULL,则返回NULL。
select JSON_TYPE('[1,2]'); -- ARRAY
For a non-NULL, non-error result, the following list describes the possible - JSON_TYPE() return values:
Purely JSON types:
OBJECT: JSON objectsARRAY: JSON arraysBOOLEAN: The JSON true and false literalsNULL: The JSON null literal
Numeric types:
- INTEGER: MySQL TINYINT, SMALLINT, MEDIUMINT and INT and BIGINT scalars
DOUBLE: MySQL DOUBLE FLOAT scalarsDECIMAL: MySQL DECIMAL and NUMERIC scalars
Temporal types:
DATETIME: MySQL DATETIME and TIMESTAMP scalarsDATE: MySQL DATE scalarsTIME: MySQL TIME scalars
String types:
STRING: MySQL utf8 character type scalars:- CHAR, VARCHAR, TEXT, ENUM, and SET
Binary types:
BLOB: MySQL binary type scalars including- BINARY, VARBINARY, BLOB, and BIT
JSON_EXTRACT(json_doc, path[, path] ...)
从json文档里抽取数据。如果有参数有NULL或path不存在,则返回NULL。如果抽取出多个path,则返回的数据封闭在一个json array里
set @j2 = '[10, 20, [30, 40]]';
SELECT JSON_EXTRACT('[10, 20, [30, 40]]', '$[1]'); -- 20
SELECT JSON_EXTRACT('[10, 20, [30, 40]]', '$[1]', '$[0]'); -- [20, 10]
SELECT JSON_EXTRACT('[10, 20, [30, 40]]', '$[2][*]'); -- [30, 40]
使用方式
$.paramsName:取出一个key对应的value。
$**.paramsName 、$.[*].paramsName:取出json数组所有该字段key对应的value并以,的方式拼接在一起
SELECT
userId,
JSON_EXTRACT(loginInfo, "$.cellphone") cellphone,
JSON_EXTRACT(loginInfo, "$.wxchat") wxchat
FROM UserLogin;

select json_extract(t.loginInfo, "$**.cellphone") from userlogin t;

JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
查询包含指定字符串的paths,并作为一个json array返回。如果有参数为NUL或path不存在,则返回NULL。
one_or_all:"one"表示查询到一个即返回;"all"表示查询所有。
search_str:要查询的字符串。 可以用LIKE里的'%'或‘_’匹配。
path:在指定path下查。
SET @j3 = '["abc", [{"k": "10"}, "def"], {"x":"abc"}, {"y":"bcd"}]';
SELECT JSON_SEARCH(@j3, 'one', 'abc'); -- "$[0]"
SELECT JSON_SEARCH(@j3, 'all', 'abc'); -- ["$[0]", "$[2].x"]
SELECT JSON_SEARCH(@j3, 'all', 'abc', NULL, '$[2]'); -- "$[2].x"
SELECT JSON_SEARCH(@j3, 'all', '10'); -- "$[1][0].k"
SELECT JSON_SEARCH(@j3, 'all', '%b%'); -- ["$[0]", "$[2].x", "$[3].y"]
SELECT JSON_SEARCH(@j3, 'all', '%b%', NULL, '$[2]'); -- "$[2].x"
JSON_CONTAINS(target, candidate[, path])
判断candidate的是否被包含在target中
target和candidate为json document
如果提供了path参数,则判断target中指定路径下是否包含candidate
包含返回1,未包含返回0
SET @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
SET @j2 = '1';
SELECT JSON_CONTAINS(@j, @j2, '$.a'); -- 1
SELECT JSON_CONTAINS(@j, @j2, '$.b'); -- 0
SET @j2 = '{"d": 4}';
SELECT JSON_CONTAINS(@j, @j2, '$.a'); -- 0
SELECT JSON_CONTAINS(@j, @j2, '$.c'); -- 1
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)
查询是否存在指定路径,存在则返回1,否则返回0。如果有参数为NULL,则返回NULL。
one_or_all只能取值"one"或"all",one表示只要有一个存在即可;all表示所有的都存在才行。
JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
在指定path的json array尾部追加val。
如果指定path是一个json object,则将其封装成一个json array再追加。
如果有参数为NULL,则返回NULL。
SET @j4 = '["a", ["b", "c"], "d"]';
SELECT JSON_ARRAY_APPEND(@j4, '$[1][0]', 3); -- ["a", [["b", 3], "c"], "d"]
SET @j5 = '{"a": 1, "b": [2, 3], "c": 4}';
SELECT JSON_ARRAY_APPEND(@j5, '$.b', 'x'); -- {"a": 1, "b": [2, 3, "x"], "c": 4}
SELECT JSON_ARRAY_APPEND(@j5, '$.c', 'y'); -- {"a": 1, "b": [2, 3], "c": [4, "y"]}
SELECT JSON_ARRAY_APPEND(@j5, '$', 'z'); -- [{"a": 1, "b": [2, 3], "c": 4}, "z"]
JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
在path指定的json array元素插入val,原位置及以右的元素顺次右移。如果path指定的数据非json array元素,则略过此val;如果指定的元素下标超过json array的长度,则插入尾部。
SET @j6 = '["a", {"b": [1, 2]}, [3, 4]]';
SELECT JSON_ARRAY_INSERT(@j6, '$[1]', 'x'); -- ["a", "x", {"b": [1, 2]}, [3, 4]]
SELECT JSON_ARRAY_INSERT(@j6, '$[100]', 'x'); -- ["a", {"b": [1, 2]}, [3, 4], "x"]
SELECT JSON_ARRAY_INSERT(@j6, '$[1].b[0]', 'x'); -- ["a", {"b": ["x", 1, 2]}, [3, 4]]
SELECT JSON_ARRAY_INSERT(@j6, '$[0]', 'x', '$[3][1]', 'y'); -- ["x", "a", {"b": [1, 2]}, [3, "y", 4]]
JSON_ARRAY(val1,val2,val3...)
生成一个包含指定元素的json数组。
SELECT JSON_ARRAY(1, "abc", NULL, TRUE, CURTIME()); -- [1, "abc", null, true, "10:37:08.000000"]
JSON_INSERT(json_doc, path, val[, path, val] ...)
在指定path下插入数据,如果path已存在,则忽略此val(不存在才插入)
SET @j7 = '{ "a": 1, "b": [2, 3]}';
SELECT JSON_INSERT(@j7, '$.a', 10, '$.c', '[true, false]'); -- {"a": 1, "b": [2, 3], "c": "[true, false]"}
JSON_REPLACE(json_doc, path, val[, path, val] ...)
替换指定路径的数据,如果某个路径不存在则略过(存在才替换)。
如果有参数为NULL,则返回NULL。
SELECT JSON_REPLACE(@j7, '$.a', 10, '$.c', '[true, false]'); -- {"a": 10, "b": [2, 3]}
JSON_SET(json_doc, path, val[, path, val] ...)
设置指定路径的数据(不管是否存在)。如果有参数为NULL,则返回NULL。
SELECT JSON_SET(@j7, '$.a', 10, '$.c', '[true, false]'); -- {"a": 10, "b": [2, 3], "c": "[true, false]"}
JSON_MERGE(json_doc, json_doc[, json_doc] ...)
merge多个json文档。规则如下:
如果都是json array,则结果自动merge为一个json array;
如果都是json object,则结果自动merge为一个json object;
如果有多种类型,则将非json array的元素封装成json array再按照规则一进行mege。
SELECT JSON_MERGE('[1, 2]', '[true, false]'); -- [1, 2, true, false]
SELECT JSON_MERGE('{"name": "x"}', '{"id": 47}'); -- {"id": 47, "name": "x"}
SELECT JSON_MERGE('1', 'true'); -- [1, true]
SELECT JSON_MERGE('[1, 2]', '{"id": 47}'); -- [1, 2, {"id": 47}]
JSON_REMOVE(json_doc, path[, path] ...)
移除指定路径的数据,如果某个路径不存在则略过此路径。
如果有参数为NULL,则返回NULL。
SET @j8 = '["a", ["b", "c"], "d"]';
SELECT JSON_REMOVE(@j8, '$[1]'); -- ["a", "d"]
# update act_ru_variable's TEXT_ for each ProcessInstanceID
with json_obj_location as (
select inx1,
if(json_contains(json_extract(j_inx.TEXT_, concat(inx2, '.', 'roleIds')), json_array('POST_PERSON')), inx2, '$[999]') inx2,
inx3,
PROC_INST_ID_,
NAME_
from (
select substring_index(json_unquote(json_search(c.TEXT_, 'one', 'risk_confirm_modify')), '.', 1) as inx1,
substring_index(json_unquote(json_search(c.TEXT_, 'one', 'risk_finnal_confirm')), '.', 1) as inx2,
substring_index(json_unquote(json_search(c.TEXT_, 'one', 'create')), '.', 1) as inx3,
c.PROC_INST_ID_,
c.NAME_,
c.TEXT_
from act_ru_variable c
where c.PROC_INST_ID_ in (${processInsID})
and c.NAME_ = 'users'
and c.TASK_ID_ is null) j_inx
)
update act_ru_variable uc inner join json_obj_location js on js.NAME_ = uc.NAME_ and js.PROC_INST_ID_ = uc.PROC_INST_ID_
set uc.TEXT_= (select a.json_str_result
from (select json_replace(c.TEXT_,
concat(js.inx1, '.', 'userIds'), json_array('111111'),
concat(js.inx1, '.', 'userNames'), json_array('Chris'),
concat(js.inx2, '.', 'userIds'), json_array('111111'),
concat(js.inx2, '.', 'userNames'), json_array('Chris'),
concat(js.inx3, '.', 'userIds'), json_array('111111'),
concat(js.inx3, '.', 'userNames'), json_array('Chris')) as json_str_result
from act_ru_variable c
where c.PROC_INST_ID_ = js.PROC_INST_ID_
and c.NAME_ = js.NAME_
and c.TASK_ID_ is null) a)
where uc.PROC_INST_ID_ = js.PROC_INST_ID_
and uc.NAME_ = js.NAME_
and uc.TASK_ID_ is null;
# check act_ru_variable result after update
select cast(v.TEXT_ as json) from act_ru_variable v where v.PROC_INST_ID_ in (${processInsID}) and v.NAME_ = 'users' and v.TASK_ID_ is null ;
| 创建时间: | 2021/12/27 17:40 |
| 更新时间: | 2022/6/2 11:38 |
| 作者: | Chris |
| 来源: | https://www.szlib.org.cn/Search/searchdetail.jsp?v_tablearray=bibliosm,serbibm,apabibibm,mmbibm,&v_recno=5136156&v_curtable=bibliosm&site=null |







| 创建时间: | 2022/5/26 15:39 |
| 更新时间: | 2022/5/30 19:57 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/ywb201314/article/details/106801253/ |
将名称按词典中的词条转成全英文词条
例如 :开始基于数据库进行数据比对 转成 START_BASE_DATA_DATA_COMPARE
sb是一个StringBuffer,replaceContext待替换的字符串,这个方法会把匹配到的内容替换为replaceContext,并且把从上次替换的位置到这次替换位置之间的字符串也拿到,然后,加上这次替换后的结果一起追加到StringBuffer里
假如这次替换是第一次替换,那就是只追加替换后的字符串啦。
matcher.appendReplacement(sb, replaceContent):
sb是一个StringBuffer,这个方法是把最后一次匹配到内容之后的字符串追加到StringBuffer中。
matcher.appendTail(sb);
例子:
@Test
public void buildCode() {
List<String> nameLines = getNameLines();
Map<String, String> codeMap = getCodeMap();
for (String nameLine : nameLines) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : codeMap.entrySet()) {
Pattern p = Pattern.compile(entry.getKey());
Matcher m = p.matcher(nameLine);
while (m.find()) {
m.appendReplacement(sb, "_" + entry.getValue() + "_");
}
m.appendTail(sb);
nameLine = sb.toString();
sb.setLength(0);
}
String result = nameLine;
result = result.replaceAll("__", "_");
String suffix = result.substring(result.length() - 1);
if (suffix.equals("_")) {
result = result.substring(0, result.length() - 1);
}
System.out.println(result.toUpperCase(Locale.ROOT));
}
}
| 创建时间: | 2022/5/30 13:27 |
| 来源: | https://mp.weixin.qq.com/s/CHloraZS8ls-lVfgahFC-A |
| 创建时间: | 2022/5/27 15:01 |
| 来源: | https://mp.weixin.qq.com/s/khEgG4n2WuQSYSBkJCWZMA |
| 创建时间: | 2022/5/26 19:51 |
| 更新时间: | 2022/5/26 20:03 |
| 作者: | Chris |
- 使用
dependencyManagement可以统一管理项目的版本号,确保应用的各个项目的依赖和版本一致,不用每个模块项目都弄一个版本号,不利于管理.- 当需要变更版本号的时候只需要在父类容器里更新,不需要任何一个子项目的修改;
- 如果某个子项目需要另外一个特殊的版本号时,只需要在自己的模块dependencies中声明一个版本号即可。子类就会使用子类声明的版本号,不继承于父类版本号。
与
dependencies区别:
| 创建时间: | 2022/5/24 13:34 |
| 来源: | https://mp.weixin.qq.com/s/Q83gNcpqN8sydGKZMXUq2A |
| 创建时间: | 2022/5/23 10:21 |
| 更新时间: | 2022/5/23 19:36 |
| 来源: | https://mp.weixin.qq.com/s/VLtQNsqcH8WN_aTrulBGTQ |


| fastjson特性说明 | fastjson枚举 | fastjson默认状态 | jackson枚举 | jackson默认状态 | jackson特性说明 |
| Parser close时自动关闭为创建Parser实例而创建的底层InputStream以及Reader等输入流 | Feature.AutoCloseSource | 开启 | JsonParser.Feature.AUTO_CLOSE_SOURCE | 开启 | 保持开启 |
| 允许json字符串中带注释 | Feature.AllowComment | 关闭 | JsonParser.Feature.ALLOW_COMMENTS | 关闭 | 根据系统的json数据情况开启 |
| 允许json字段名不被引号包括起来 | Feature.AllowUnQuotedFieldNames | 开启 | JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES | 关闭 | 根据系统的json数据情况开启 |
| 允许json字段名使用单引号包括起来 | Feature.AllowSingleQuotes | 开启 | JsonParser.Feature.ALLOW_SINGLE_QUOTES | 关闭 | 根据系统的json数据情况开启 |
| 将json字段名作为字面量缓存起来,即fieldName.intern() | Feature.InternFieldNames | 开启 | - | - | jackson默认使用InternCache缓存了PropertyName |
| 识别ISO8601格式的日期字符串,例如:2018-05-31T19:13:42.000Z或2018-05-31T19:13:42.000+07:00 | Feature.AllowISO8601DateFormat | 关闭 | - | - | jackson默认支持ISO8601格式日期字符串的解析,并且也可以通过ObjectMapper.setDateFormat指定解析格式 |
| 忽略json中包含的连续的多个逗号,非标准特性 | Feature.AllowArbitraryCommas | 关闭 | - | - | jackson不支持该特性,且该特性是非标准特性,因此可以忽略 |
| 将json中的浮点数解析成BigDecimal对象,禁用后会解析成Double对象 | Feature.UseBigDecimal | 开启 | DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS | 关闭 | 建议开启 |
| 解析时忽略未知的字段继续完成解析 | Feature.IgnoreNotMatch | 开启 | DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES | 开启 | jackson默认开启遇到未知属性需要抛异常,因此如要和fastjson保持一致则需要关闭该特性 |
| 如果你用fastjson序列化的文本,输出的结果是按照fieldName排序输出的,parser时也能利用这个顺序进行优化读取。这种情况下,parser能够获得非常好的性能 | Feature.SortFeidFastMatch | 关闭 | - | - | fastjson内部处理逻辑,jackson不支持该特性,不影响功能 |
| 禁用ASM | Feature.DisableASM | 关闭 | - | - | fastjson内部处理逻辑,jackson不支持该特性,不影响功能 |
| 禁用循环引用检测 | Feature.DisableCircularReferenceDetect | 关闭 | - | - | fastjson内部处理逻辑,jackson不支持该特性,不影响功能 |
| 对于没有值的字符串属性设置为空串 | Feature.InitStringFieldAsEmpty | 关闭 | - | - | jackson不支持该特性,但是可以通过@JsonSetter的nulls()和contentNulls()分别设置Bean以及Array/Collection的元素对null的处理方式。例如Nulls.AS_EMPTY就会将null设置为JsonDeserializer.getEmptyValue |
| 非标准特性,允许将数组按照字段顺序解析成Java Bean,例如"[1001,\"xx\",33]"可以等价为"{\"id\": 10001, \"name\": \"xx\", \"age\": 33}" | Feature.SupportArrayToBean | 关闭 | - | - | 非标准特性,且使用场景较少,jackson不支持该特性 |
| 解析后属性保持原来的顺序 | Feature.OrderedField | 关闭 | - | - | - |
| 禁用特殊字符检查 | Feature.DisableSpecialKeyDetect | 关闭 | - | - | - |
| 使用对象数组而不是集合 | Feature.UseObjectArray | 关闭 | DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY | 关闭 | 保持关闭 |
| 支持解析没有setter方法的非public属性 | Feature.SupportNonPublicField | 关闭 | - | - | jaskson可以通过ObjectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)来达到相同的目的 |
| 禁用fastjson的AUTOTYPE特性,即不按照json字符串中的@type自动选择反序列化类 | Feature.IgnoreAutoType | 关闭 | - | - | jackson的PolymorphicDeserialization默认是支持Object.class、abstract classes、interfaces属性的AUTO Type,但是该特性容易导致安全漏洞,强烈建议使用ObjectMapper.disableDefaultTyping()设置为只允许@JsonTypeInfo生效 |
| 禁用属性智能匹配,例如下划线自动匹配驼峰等 | Feature.DisableFieldSmartMatch | 关闭 | - | - | jackson可以通过ObjectMapper.setPropertyNamingStrategy()达到相同的目的,但这种是针对一个json串的统一策略,如果要在一个json串中使用不同的策略则可以使用@JsonProperty.value()指定字段名 |
| 启用fastjson的autotype功能,即根据json字符串中的@type自动选择反序列化的类 | Feature.SupportAutoType | 关闭 | ObjectMapper.DefaultTyping.* | 开启 | jackson的PolymorphicDeserialization支持不同级别的AUTO TYPE,但是这个功能容易导致安全漏洞,强烈建议使用ObjectMapper.disableDefaultTyping()设置为只允许@JsonTypeInfo生效 |
| 解析时将未用引号包含的json字段名作为String类型存储,否则只能用原始类型获取key的值。例如String text="{123:\"abc\"}"在启用了NonStringKeyAsString后可以通过JSON.parseObject(text).getString("123")的方式获取到"abc",而在不启用NonStringKeyAsString时,JSON.parseObject(text).getString("123")只能得到null,必须通过JSON.parseObject(text).get(123)的方式才能获取到"abc"。 | Feature.NonStringKeyAsString | 关闭 | - | - | 非标准特性,jackson并不支持 |
| 自定义"{\"key\":value}"解析成Map实例,否则解析为JSONObject | Feature.CustomMapDeserializer | 关闭 | - | - | jackson没有相应的全局特性,但是可以通过TypeReference达到相同的效果 |
| 枚举未匹配到时抛出异常,否则解析为null | Feature.ErrorOnEnumNotMatch | 关闭 | DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL | 关闭 | fastjson默认解析为null,jackson则相反,默认会抛异常,建议采用jackson默认行为 |
| fastjson特性说明 | fastjson枚举 | fastjson默认状态 | jackson枚举 | jackson默认状态 | jackson特性说明 |
| 输出的json字段名被引号包含 | SerializerFeature.QuoteFieldNames | 开启 | JsonGenerator.Feature.QUOTE_FIELD_NAMES | 开启 | 保持开启 |
| 序列化时使用单引号,而不是使用双引号 | SerializerFeature.UseSingleQuotes | 关闭 | - | - | jackson不支持该特性 |
| 序列化时,value为null的key或field也输出 | SerializerFeature.WriteMapNullValue | 关闭 | JsonInclude.Include.ALWAYS | 开启 | 建议按需选择。注意SerializationFeature.WRITE_NULL_MAP_VALUES从2.9已废弃,且会被JsonInclude.Include给覆盖 |
| 序列化枚举时使用枚举类型的toString()方法,和SerializerFeature.WriteEnumUsingName互斥 | SerializerFeature.WriteEnumUsingToString | 关闭 | SerializationFeature.WRITE_ENUMS_USING_TO_STRING | 关闭 | 建议关闭,或者和反序列化的DeserializationFeature.READ_ENUMS_USING_TO_STRING保持一致 |
| 序列化枚举时使用枚举类型的name()方法,和SerializerFeature.WriteEnumUsingToString互斥 | SerializerFeature.WriteEnumUsingName | 开启 | - | - | jackson的默认行为,无需配置 |
| 序列化时对Date、Calendar等类型使用ISO8601格式进行格式化,否则以timestamp形式输出Long数字 | SerializerFeature.UseISO8601DateFormat | 关闭 | SerializationFeature.WRITE_DATES_AS_TIMESTAMPS | 开启 | jackson和fastjson的默认行为都是将Date数据输出为Long,建议根据不同的场景选择是否需要格式化日期 |
| 序列化List类型数据时将null输出为"[]" | SerializerFeature.WriteNullListAsEmpty | 关闭 | - | - | 可以通过PropertyFilter/SerializerFactory.withSerializerModifier(BeanSerializerModifier)任一一种方式达到相同效果,推荐使用PropertyFilter |
| 序列化String类型的field时将null输出为"" | SerializerFeature.WriteNullStringAsEmpty | 关闭 | - | - | 可以通过PropertyFilter/SerializerFactory.withSerializerModifier(BeanSerializerModifier)任一一种方式达到相同效果,推荐使用PropertyFilter |
| 序列化Number类型的field时将null输出为0 | SerializerFeature.WriteNullNumberAsZero | 关闭 | - | - | 可以通过PropertyFilter/SerializerFactory.withSerializerModifier(BeanSerializerModifier)任一一种方式达到相同效果,推荐使用PropertyFilter |
| 序列化Boolean类型的field时将null输出为false | SerializerFeature.WriteNullBooleanAsFalse | 关闭 | - | - | 可以通过PropertyFilter/SerializerFactory.withSerializerModifier(BeanSerializerModifier)任一一种方式达到相同效果,推荐使用PropertyFilter |
| 序列化时忽略transient修饰的field | SerializerFeature.SkipTransientField | 开启 | MapperFeature.PROPAGATE_TRANSIENT_MARKER | 关闭 | 建议保持关闭,通过@JsonIgnore或者FilterProvider来指定忽略的属性 |
| 序列化时,如果未指定order,则将field按照getter方法的字典顺序排序 | SerializerFeature.SortField | 开启 | MapperFeature.SORT_PROPERTIES_ALPHABETICALLY | 关闭 | 建议关闭,排序会影响序列化性能(fastjson在反序列化时支持按照field顺序读取解析,因此排序后的json串有利于提高fastjson的解析性能,但jackson并没有该特性) |
| 把\t做转义输出,已废弃,即使开启也无效 | SerializerFeature.WriteTabAsSpecial | 关闭 | - | - | - |
| 格式化json输出 | SerializerFeature.PrettyFormat | 关闭 | SerializationFeature.INDENT_OUTPUT | 关闭 | 建议保持关闭,格式化可以交给前端完成 |
| 序列化时把类型名称写入json | SerializerFeature.WriteClassName | 关闭 | - | - | jackson可以通过@JsonTypeInfo达到类似的效果,参见Jackson Annotation Examples |
| 序列化时消除对同一对象循环引用的问题 | SerializerFeature.DisableCircularReferenceDetect | 关闭 | SerializationFeature.FAIL_ON_SELF_REFERENCES | 开启 | 保持开启,避免循环引用 |
| 对斜杠'/'进行转义 | SerializerFeature.WriteSlashAsSpecial | 关闭 | - | - | jackson可以通过自定义Serializer实现相同效果,按需设置 |
| 将中文都会序列化为\uXXXX格式,字节数会多一些,但是能兼容IE 6 | SerializerFeature.BrowserCompatible | 关闭 | - | - | jackson可以通过自定义Serializer实现相同效果,按需设置 |
| 全局修改日期格式,默认使用JSON.DEFFAULT_DATE_FORMAT | SerializerFeature.WriteDateUseDateFormat | 关闭 | - | - | jackson可以通过@JsonFormat.pattern()、ObjectMapper.setDateFormat()等方式实现相同效果 |
| 序列化时不把最外层的类型名称写入json | SerializerFeature.NotWriteRootClassName | 关闭 | - | - | jackson可以通过@JsonRootName达到类似的效果,参见Jackson Annotation Examples |
| 不转义特殊字符,已废弃,即使开启也无效 | SerializerFeature.DisableCheckSpecialChar | 关闭 | - | - | - |
| 将Bean序列化时将field值按顺序当成json数组输出,而不是json object,同时不会输出fieldName,例如:{"id":123,"name":"xxx"}会输出成[123,"xxx"] | SerializerFeature.BeanToArray | 关闭 | - | - | 非标准特性,jackson并不支持 |
| 序列化Map时将非String类型的key作为String类型输出,例如:{123:231}会输出成{"123":231} | SerializerFeature.WriteNonStringKeyAsString | 关闭 | - | - | 非标准特性,jackson并不支持 |
| 序列化Byte、Short、Integer、Long、Float、Double、Boolean及其对应原始类型field时,如果属性值为各自类型的默认值(如0、0F、0L),则不会输出该属性 | SerializerFeature.NotWriteDefaultValue | 关闭 | - | - | 非标准特性,jackson并不支持 |
| 序列化时将(、)、>、<以unicode编码输出 | SerializerFeature.BrowserSecure | 关闭 | - | - | jackson可以通过自定义Serializer实现相同效果,按需设置,通常可以交给前端处理 |
| 序列化时忽略没有实际属性对应的getter方法 | SerializerFeature.IgnoreNonFieldGetter | 关闭 | - | - | - |
| 序列化时把非String类型数据当作String类型输出 | SerializerFeature.WriteNonStringValueAsString | 关闭 | - | - | jackson有一个类似的特性JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS可以将数字作为字符串输出,但没有覆盖所有非String类型 |
| 序列化时忽略会抛异常的getter方法 | SerializerFeature.IgnoreErrorGetter | 关闭 | - | - | - |
| 序列化时将BigDecimal使用toPlainString()输出 | SerializerFeature.WriteBigDecimalAsPlain | 关闭 | JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN | 关闭 | 按需开启 |
| 序列化时对Map按照Key进行排序 | SerializerFeature.MapSortField | 关闭 | SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS | 关闭 | 建议关闭,开启会影响性能 |
larva-zhang/jackson-datatype-fastjson欢迎大家使用或提issues。
| 创建时间: | 2022/5/23 10:20 |
| 更新时间: | 2022/5/23 19:32 |
| 来源: | https://mp.weixin.qq.com/s/m9Q_fm6KwTBClA0GPoJaRg |
mybatis-plus
https://baomidou.com/guide/
https://github.com/baomidou/mybatis-plus
http://www.baizhiedu.xin/front/index#/main
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
内置分页插件
内置性能分析插件
支持 Lambda 形式调用
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
建项目
<groupId>com.chris.mybatis-plus</groupId>
<artifactId>mybatis-plus2020</artifactId>
改pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.chris.mybatis-plus</groupId>
<artifactId>mybatis-plus2020</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-plus2020</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis.plus.version>3.2.0</mybatis.plus.version>
<mysql.connector.version>6.0.6</mysql.connector.version>
<druid-version>1.1.10</druid-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
建yml
server:
port: 4003
servlet:
context-path: /api
tomcat:
uri-encoding: UTF-8
spring:
application:
name: mybatis-plus2020
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.140.127:3306/chris?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
username: root
password: 65536
druid:
#初始化时建立物理连接的个数
initial-size: 5
#最小连接池数量
min-idle: 5
#最大连接池数量 maxIdle已经不再使用
max-active: 20
#获取连接时最大等待时间,单位毫秒
max-wait: 60000
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
#既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000
#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
min-evictable-idle-time-millis: 30000
#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
validation-query: select 'x'
#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
#当数据库抛出不可恢复的异常时,抛弃该连接
#exception-sorter: true
#是否缓存preparedStatement,mysql5.5+建议开启
pool-prepared-statements: true
#当值大于0时poolPreparedStatements会自动修改为true
max-pool-prepared-statement-per-connection-size: 20
#配置扩展插件
filters: stat,wall,slf4j,config
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=false;config.decrypt.key=
#合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
#设置访问druid监控页的账号和密码,默认没有
stat-view-servlet:
login-username: admin
login-password: allsale
filter:
wall:
config:
comment-allow: true
enabled: true
redis:
host: master
password: 123456
port: 6379
timeout: 1000
servlet:
multipart: # 文件传输大小
max-request-size: 100MB
max-file-size: 100MB
mybatis-plus:
mapper-locations: classpath:mappers/*.xml
global-config:
db-config:
#驼峰下划线转换
#column-underline: true
#逻辑删除配置
logic-delete-value: I
logic-not-delete-value: A
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#为所有Entity类所在包起默认别名
type-aliases-package: com.chris.mybatisplus.entities
logging:
level:
#根日志级别
root: info
#数据查询接口级别
com.chris.mybatisplus.dao.mapper: debug
主启动
package com.chris.mybatisplus;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan ("com.chris.mybatisplus.dao.mapper")
public class MybatisPlusMain {
public static void main(String[] args) {a
SpringApplication.run(MybatisPlusMain.class, args);
}
}
接口类
package com.chris.mybatisplus.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chris.mybatisplus.entities.User;
// 使用mybatis-plus增强接口
public interface UserMapper extends BaseMapper<User> {
}
业务类
package com.chris.mybatisplus.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors (chain = true)
public class User {
private Integer id;
private String name;
private Integer age;
private Date bir;
}
测试类
package com.chris.mybatis;
import com.chris.mybatisplus.MybatisPlusMain;
import com.chris.mybatisplus.dao.mapper.UserMapper;
import com.chris.mybatisplus.entities.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest (classes = MybatisPlusMain.class)
public class TestMybaisPlus {
@Autowired
public UserMapper userMapper;
//查询所有
@Test
public void testFindAll() {
List<User> users = userMapper.selectList(null);
users.forEach(user -> System.out.println("user:" + user));
}
}
如果类名与表名一致时,@TableName可以省略
@TableName("t_user") //在不配置的情况下默认表名与类名一致
public class User
@TableId代表主键,也可以完成主键的生成策略
/**
* AUTO:数据库自增
* INPUT:inset前自行设置主键值
* ASSIGN_ID:分配ID,主键类型为Number(Integer和Long)使用接口IdentifierGenerator的nextId方法(默认实现类为DefaultIdentifierGenerator雪花算法)
* ASSIGN_UUID:分配UUID,主键类型为String,使用接口IdentifierGenerator的nextId方法(默认default方法)
*/
@TableId(value = "tableId", type = IdType.AUTO)
private Integer id;
@TableField表中列名与Java类中属性的映射
/**
* 表中列名与Java类中属性的映射
* 如果列名和属性名一致可以不用配置此注解
*/
@TableField("last_name")
private String name;
配置MybatisPlusConfig
package com.chris.mybatisplus.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@MapperScan("com.chris.mybatisplus.dao.mapper")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
测试类
@SpringBootTest(classes = MybatisPlusMain.class)
public class PaginationTest {
@Autowired
private UserMapper userMapper;
@Test
public void paginationSelect() {
//参数1:当前页数,参数2:每页记录数
IPage<User> page = new Page<>(2, 2);
// queryWrapper没有设置条件则查询所有记录
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
IPage<User> ipage = userMapper.selectPage(page, queryWrapper);
long total = ipage.getTotal();
System.out.println("total:" + total);
ipage.getRecords().forEach(user -> System.out.println("user:" + user));
}
}
注意
目前分页插件只能支持单表查询,多表关联查询时分页插件不可用
改pom
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
yml
数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
#初始化时建立物理连接的个数
initial-size: 5
#最小连接池数量
min-idle: 5
#最大连接池数量 maxIdle已经不再使用
max-active: 20
#获取连接时最大等待时间,单位毫秒
max-wait: 60000
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
#既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000
#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
min-evictable-idle-time-millis: 30000
#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
validation-query: select 'x'
#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
#当数据库抛出不可恢复的异常时,抛弃该连接
#exception-sorter: true
#是否缓存preparedStatement,mysql5.5+建议开启
pool-prepared-statements: true
#当值大于0时poolPreparedStatements会自动修改为true
max-pool-prepared-statement-per-connection-size: 20
#配置扩展插件
filters: stat,wall,slf4j,config
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=false;config.decrypt.key=
#合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
#设置访问druid监控页的账号和密码,默认没有
stat-view-servlet:
login-username: admin
login-password: allsale
filter:
wall:
config:
comment-allow: true
enabled: true
dynamic:
#指定默认数据源
datasource:
#配置了两个数据源
master:
url: jdbc:mysql://192.168.140.127:3306/chris?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
username: root
password: 65536
driverClassName: com.mysql.cj.jdbc.Driver
cluster:
url: jdbc:mysql://192.168.140.127:3306/slave01?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
username: root
password: 65536
driverClassName: com.mysql.cj.jdbc.Driver
primary: master
profile
spring:
application:
name: mybatis-plus2020
profiles:
active: multidatasource

接口类
public interface UserService {
List<User> findAll();
int save(User user);
}
实现类
数据源的切换要在service层,在dao层不能实现数据源的自由切换
@DS()用来切换数据源作用在类上和方法上
如果不在这个注解,默认使用primary: master上配置的数据源
如果方法和类上都加了这个注解,以方法上的注解优先,遵循局部优先原则
@Service
@DS("master")
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public List<User> findAll() {
return userMapper.selectList(null);
}
@Override
@DS("cluster")
public int save(User user) {
return userMapper.insert(user);
}
}
测试类
@SpringBootTest(classes = MybatisPlusMain.class)
public class MultiDataSource {
@Resource
private UserService userService;
@Test
public void testFindAll() {
userService.findAll().parallelStream().forEach(user -> System.out.println("user:" + user));
}
@Test
public void testSave() {
User user = new User();
user.setBir(new Date()).setAge(22).setName("Sopher");
userService.save(user);
}
}
很多情况下我们的系统都需要逻辑删除,方便恢复查找误删除的数据。
通过 mybatis-plus 可以通过全局配置的方式,而不需要再去手动处理。针对更新和查询操作有效,新增不做限制。
方式1
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不使用@TableLogic)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
方式2 通过注解
@TableLogic
@TableFeild当中有个属性叫做 fill,通过FieldFill设置属性,这个就是做自动填充用的
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入填充字段
*/
INSERT,
/**
* 更新填充字段
*/
UPDATE,
/**
* 插入和更新填充字段
*/
INSERT_UPDATE
}
但是这个直接是不能使用的,需要通过实现 mybatis-plus 提供的接口,增加如下配置启动自动填充功能:
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 起始版本 3.3.0(推荐使用)
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
// 起始版本 3.3.0(推荐)
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
/**
* 时间字段,自动添加
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
| 创建时间: | 2022/11/9 23:19 |
| 更新时间: | 2022/11/9 23:24 |
| 作者: | Chris |
https://www.jianshu.com/p/925dba9f5969


| 创建时间: | 2020/9/8 14:01 |
| 更新时间: | 2022/11/6 18:39 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/forezp/article/details/84313907 |
https://blog.csdn.net/forezp/article/details/84313907
@Configuration
public class Bean1Config {
@Bean
@ConditionalOnBean(value = Bean2.class )
public Bean1 bean1(){
return new Bean1();
}
}
@Configuration
public class Bean2Config {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}
@GetMapping("/getBean/{beanName}")
public void getBean(@PathVariable String beanName) {
Object bean = SpringUtil.getApplicationContext().getBean(beanName);
if (Objects.nonNull(bean)) {
System.out.println(beanName + " init success");
} else {
System.out.println(beanName + "init fail");
}
}
代码中bean1是定义在配置类中的,当执行到配置类解析的时候,@Component,@Service,@Controller ,@Configuration标注的类已经全部扫描,所以这些BeanDifinition已经被同步。 但是bean1的条件注解依赖的是bean2,bean2是被定义的配置类中的,所以此时配置类的解析无法保证先后顺序,就会出现不生效的情况。
在spring ioc的过程中,优先解析@Component,@Service,@Controller注解的类。其次解析配置类,也就是@Configuration标注的类。最后开始解析配置类中定义的bean
解决方法
- ConditionalOnClass(Bean2.class)来代替。
- 如果一定要区分两个配置类的先后顺序,可以将这两个类交与EnableAutoConfiguration管理和触发。也就是定义在META-INF\spring.factories中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter AutoConfigureOrder控制先后顺序。之所以这么做是因为这三个注解只对自动配置类的先后顺序生效
| 创建时间: | 2022/11/6 15:24 |
| 更新时间: | 2022/11/6 16:01 |
| 作者: | Chris |
| 来源: | https://www.jianshu.com/p/4b5eb29cc6d9 |
https://www.jianshu.com/p/4b5eb29cc6d9
TransactionSynchronizationManager事务同步管理器。我们可以自定义实现
TransactionSynchronization,监听Spring的事务操作。可以在事务提交之后,回调TransactionSynchronization类的方法。
在源码中的使用
org.springframework.cache.transaction.TransactionAwareCacheDecorator#put
@Override
public void put(final Object key, @Nullable final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
});
}
else {
this.targetCache.put(key, value);
}
}
DataSourceTransactionManager请求事务方法时,调用dobegin()将事务信息保存到TransactionSynchronizationManager中:在该方法中主要是在数据库连接池中获取一个Connection对象,然后将Connection对象放入到ThreadLocal中。实际上该事务方法的信息均由TransactionSynchronizationManager类管理。
源码:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//在可见的数据源(连接池)中获取Connection对象
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
...
//关闭Connection对象的自动提交
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
//将Connection对象绑定到Thread中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
//将Connection对象绑定到resources 上。
Object oldValue = map.put(actualKey, value);
...
}}
org.mybatis.spring.transaction.SpringManagedTransaction 类直接获取Connection对象。源码:
org.mybatis.spring.transaction.SpringManagedTransaction#openConnection
private void openConnection() throws SQLException {
//在ThreadLocal中获取Connection对象
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
//在ThreadLocal中获取是否开启事务
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
...
}
源码:
org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//在doBegin()方法中,已经将创建的Connection对象放入到TransactionSynchronizationManager中
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
//直接返回Thread存储的Connection对象。
return conHolder.getConnection();
}
...
return con;
}
这个类是程序员对事务同步的扩展点:用于事务同步回调的接口
一般而言,我们在TransactionSynchronization使用最多的是
afterCommit和afterCompletion方法。
可以在事务执行完毕之后,直接调用
afterCommit()方法进行异步通知。
在 doCommit()方法中提交事务后,在cleanupAfterCompletion对connection进行重置,但依旧可以在afterCommit()回调中对数据库进行操作。
org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
//提交事务
doCommit(status);
...
try {
//回调所有事务同步器的afterCommit方法。
triggerAfterCommit(status);
}
finally {
//回调所有事务同步器的afterCompletion方法。
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}
}
finally {
//清除TransactionSynchronizationManager的ThreadLocal绑定的数据。
//解除Thread绑定的resources资源。
//将Commit设置为自动提交。
//清理ConnectionHolder资源。
cleanupAfterCompletion(status);
}
}
| 创建时间: | 2020/9/2 15:44 |
| 更新时间: | 2022/11/6 15:20 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/yanxin1213/article/details/100582643 |
Spring Framework 对事务管理提供了一致的抽象,其特点如下:
Spring支持 编程式事务管理 和 声明式事务管理 两种方式。
编程式事务管理使用TransactionTemplate
声明式事务管理建立在AOP之上的。
其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,
在执行完目标方法之后根据执行情况提交或者回滚事务。
默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。
对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring会将底层连接的自动提交特性设置为false。
org/springframework/jdbc/datasource/DataSourceTransactionManager.java
if (con.getautocommit()) {
txobject.setmustrestoreautocommit(true);
if (logger.isdebugenabled()) {
logger.debug("switching jdbc connection [" + con + "] to manual commit");
}
con.setautocommit(false);
}

MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean 引用的数据源与DataSourceTransactionManager 引用的数据源一致即可,否则事务管理会不起作用。
spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口
隔离级别是指若干个并发的事务之间的隔离程度。
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
| 常量 | 说明 |
|---|---|
| ISOLATION_DEFAULT | 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED |
| ISOLATION_READ_UNCOMMITTED | 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别 |
| ISOLATION_READ_COMMITTED | 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 |
| ISOLATION_REPEATABLE_READ | 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读 |
| ISOLATION_SERIALIZABLE | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 |
| 常量 | 说明 |
|---|---|
| PROPAGATION_REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值 |
| PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
| PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
| PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
| PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
| PROPAGATION_MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
| PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。 |
使嵌套的事务使用相同的物理事务,但是对嵌套调用设置了保存点|savepoint,所以 inner 事务可以独立于 outer 事务回滚。
如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
会创建一个新的物理事务,内层事务的提交回滚都是独立于外层事务的。
外层事务不受内层事务结果的影响,他们运行于独立的物理事务。
如果当前方法的执行上下文中已经打开了事务,那么就使用当前这个事务。
如果当前没有事务,就创建一个新的。
如果多个方法都声明了 REQUIRED,并且他们嵌套调用,那么他们会共享同一个物理事务。就是 inner 产生了回滚,那么 outer 会跟着回滚。

让spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。
spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。
可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。
还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。
@Transactional注解
| value | String | 可选的限定描述符,指定使用的事务管理器 |
|---|---|---|
| propagation | enum: Propagation | 可选的事务传播行为设置 |
| isolation | enum: Isolation | 可选的事务隔离级别设置 |
| readOnly | boolean | 读写或只读事务,默认读写 |
| timeout | int (in seconds granularity) | 事务超时时间设置 |
| rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
| rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
| noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
| noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
| 创建时间: | 2022/11/5 18:46 |
| 更新时间: | 2022/11/5 19:20 |
| 作者: | Chris |
加入下面依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--将SpringBoot产生的标准端点数据转换成prometheus需要的数据格式-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
加入如下配置信息
management:
endpoints:
web:
exposure:
include: "*" # 对外暴露所有的监控指标
base-path: /admin # actuator暴露接口的前缀,默认为 /actuator
endpoint:
prometheus:
enabled: true # 激活prometheus
health:
show-details: always # 一直显示健康详细信息
metrics:
export:
prometheus:
enabled: true # 允许导出 prometheus 中的指标信息
server:
port: 8888
@RequestMapping("/heap/test")
@RestController
public class TestController {
public static final Map<String, Object> map = new ConcurrentHashMap<>();
@RequestMapping("")
public String testHeapUsed() {
for (int i = 0; i < 1000000; i++) {
map.put(i + "", new Object());
}
return "ok";
}
}
http://localhost:8888/admin/

在向prometheus注册信息中加入应用名称信息
@Bean
MeterRegistryCustomizer<MeterRegistry> config(@Value("${spring.application.name}") String applicationName) {
return (registry) -> registry.config().commonTags("fuck", applicationName);
}
http://192.168.0.104:8888/admin/prometheus

mkdir /etc/prometheus
vi
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
evaluation_interval: 15s # Evaluate rules every 15 seconds.
# Attach these extra labels to all timeseries collected by this Prometheus instance.
external_labels:
monitor: 'codelab-monitor'
rule_files:
- 'prometheus.rules.yml'
scrape_configs:
- job_name: 'prometheus'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']
## 以下内容为springboot应该配置,每隔5s向192.168.0.104:8888的/admin/prometheus端点发起请求获取指标数据并存储在本地
- job_name: 'springboot-prometheus'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
metrics_path: '/admin/prometheus'
static_configs:
- targets: ['192.168.0.104:8888']
labels:
group: 'production'
docker run -d --name=prometheus -p 9090:9090 -v /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
http://master:9090



docker run -d --name=grafana -p 3000:3000 grafana/grafana
http://master:3000/
配置数据源


配置prometheus访问地址
http://192.168.139.127:9090

导入模板

输入
4701, 并选择刚才配置的prometheus数据源后点击导入


| 创建时间: | 2020/9/22 9:00 |
| 更新时间: | 2022/11/5 12:26 |
| 作者: | Chris |
| 来源: | https://www.bilibili.com/video/BV17g4y1v72V/?spm_id_from=333.337.search-card.all.click&vd_source=d5219f7796f600867d78e4b27b0f6350 |
Axure 9.0.0.3701,3704,3706,3707,3712,3714,3716版本使用
License:Freecrackdownload.com
Key:5vYpJgQZ431X/G5kp6jpOO8Vi3TySCBnAslTcNcKkszfPH7jaM4eKM8CrALBcEC1
ZF3R0-FHED2-M80TY-8QYGC-NPKYF
YF390-0HF8P-M81RQ-2DXQE-M2UT6
ZF71R-DMX85-08DQY-8YMNC-PPHV8
| 创建时间: | 2020/10/29 13:50 |
| 更新时间: | 2022/11/5 8:11 |
| 作者: | Chris |
| 来源: | https://www.jianshu.com/p/7d57ce4147d3 |
13622314539@P13622314539 MINGW64 /c/IdeaProjects
$ git clone https://github.com/ChrisLi716/javademo.git
Cloning into 'javademo'...
fatal: unable to access 'https://github.com/ChrisLi716/javademo.git/': SSL certificate problem: unable to get local issuer certificate
解决办法
在windows中打开git bash
ssh-keygen -t rsa -C "lilunlogic@163.com"
ssh-add ~/.ssh/id_rsa ##此命令是否执行成功不影响最终操作结果
在github中打开配置
刚生成的id_rsa.pub,将里面的内容复制,进入你的github账号,在settings下,SSH and GPG keys下new SSH key,然后将id_rsa.pub里的内容复制到Key中,完成后Add SSH Key。
验证
ssh -T git@github.com
OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054
增大缓存大小
524288000表示增至500兆,1048576000表示增至1G
git config --global http.postBuffer 524288000
利用ssh下载
git clone git://github.com/XX/XXXX.git
安全设置问题
git config http.sslVerify "false" --在项目目录里面
git config --global http.sslVerify "false" --全局设置
更新密码后更新代码报如下错,但又没有弹窗让重新输入用户名和密码
13622314539@OC136223145391 MINGW64 /c/chris/code/ipd-plm-subtr (feature0930_subtr_sc)
$ git pull origin
fatal: Authentication failed for 'http://alm.adc.com/hardware/IPD/_git/ipd-plm-subtr/'
解决办法
git config --system --unset credential.helper
使用git pull或者git push每次都需要输入用户名和密码很繁琐,耽误时间,
一条命令实现保存用户名和密码不用再输入
git config --global credential.helper store
17:20:29.687: [cloud2022] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false merge origin/master
fatal: refusing to merge unrelated histories
解决方案:
git pull origin master:master --allow-unrelated-histories
remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.
解决方案:
github->settings-> Developer settings->Personal access tokens
New personal access token
选择过期时间和权限
记得把你的token保存下来,因为你再次刷新网页的时候,你已经没有办法看到它了
https://tool.chinaz.com/dns?type=1&host=assets-cdn.github.com&ip=
github.com
github.global.ssl.fastly.net
assets-cdn.github.com
140.82.113.4 github.com
199.232.5.194 github.global.ssl.fastly.net
185.199.108.153 assets-cdn.github.com
185.199.111.153 assets-cdn.github.com
185.199.110.153 assets-cdn.github.com
185.199.109.153 assets-cdn.github.com
C:\Users\elead>ipconfig/flushdns
Windows IP 配 置
已 成 功 刷 新 DNS 解 析 缓 存 。
| 创建时间: | 2020/10/5 14:37 |
| 更新时间: | 2022/10/15 11:21 |
| 作者: | Chris |
| 来源: | https://jingyan.baidu.com/article/f79b7cb30d81109144023eba.html |
- Ctrl+ shift + P
- 然后输入 Install Package,回车
- 输入ConvertToUTF8

- Ctrl+ shift + P
- 然后输入 Install Package,回车
- 输入SFTP/FTP

Ctrl+shift+P
install package
transparency
依次输入以上命令进行安装,安装完成后可以在菜单View-Window's transparency下进行调节。
Sublime,command + shift + p
Install package,搜索 Pretty JSON
sublime菜单栏看到pretty json菜单,所以好像必须利用快捷键来使用pretty json.
Preferences > Key Bindings在右边添加pretty json的快捷键
[
{"keys": ["ctrl+alt+j"], "command": "pretty_json"},
{"keys": [ "ctrl+alt+m" ], "command": "un_pretty_json" }
]
| 创建时间: | 2022/6/10 17:40 |
| 更新时间: | 2022/10/1 14:18 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/u014042066/article/details/107134281/ |
https://blog.csdn.net/u014042066/article/details/107134281/
https://blog.csdn.net/u014042066/article/details/107134281/
org.springframework.boot.autoconfigure.EnableAutoConfiguration
- 开启Spring应该上下文的自动配置,尽可能去加载需要的配置Beans。自动配置类,一般是根据你的classpath路径来应用的。
- 当你使用了
SpringBootApplication注解,在context中自动加载配置就已经开启,不需要其他操作。- 可以通过
exclude()传入Class类型;excludeName()传入名称来排除其他类型的加载。自动加载的配置始终是在普通用户定义的Bean加载之后执行。- 默认EnableAutoConfiguration 扫描的是当前启用注解的Class对象的目录作为root目录。所以这个目录下的所有子目录都会被扫描。
- 自动装载的配置类,也是常规的SpringBean(被@Configuration修饰,
@Configuration则被@Component修饰, 如果你写的@Configuration在启动类的包名下,开启注解扫描的情况下,也是会把@Configuration注册为Bean对象。- 不在扫描包目录中也可以通过SpringFactoriesLoader的机制来定位到对应的org.springframework.boot.autoconfigure.EnableAutoConfiguration类()。
- 一般配合
Conditional、ConditionalOnClass、ConditionalOnMissingBean一起使用。
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration


org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#
getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
会去配置文件F:\mvn_repo\org\springframework\boot\spring-boot-autoconfigure\2.6.3\spring-boot-autoconfigure-2.6.3.jar!\META-INF\spring.factories 中读取 EnableAutoConfiguration下面的所有配置类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
...
org.springframework.core.io.support.SpringFactoriesLoader#loadFactoryNames
org.springframework.core.io.support.SpringFactoriesLoader#loadSpringFactories
java.util.stream.Collectors#collectingAndThen
java.util.Map#computeIfAbsent
org.springframework.context.support.AbstractApplicationContext#refresh
| 创建时间: | 2020/9/2 14:47 |
| 更新时间: | 2022/9/19 19:33 |
| 作者: | Chris |
| 来源: | https://www.jianshu.com/p/fe7198e2d635 |
java异常可以分为两种类型:系统错误、异常
系统错误由Java虚拟机抛出,用Error类表示
例如 Java虚拟机崩溃。这种情况仅凭程序自身是无法处理的,在程序中也不会对Error异常进行捕捉和抛出。
异常分为:检查异常 CheckedException和非检查异常 UncheckedException
CheckedException 来自于Exception且非运行时异常都是检查异常。
CheckedException 需要声明在方法或者构造器的抛出异常语句部分
public void methodA() throws xxx,从而在方法或构造器执行出现异常时,传播到该方法或构造器之外编译器会强制检查并通过try-catch块来对其捕获,或者在方法头声明该异常,交给调用者处理。
例如: IOException
UncheckedException 包含 RuntimeException 及其子类。
Unchecked Exception 不需要在方法或构造器上声明异常抛出的原因( public void methodA() {} ),从而在该方法或构造器执行出现异常时,传播到该方法或构造器 之外
RuntimeException 是指程序运行过程中才会被检查的异常
例如:类型错误转换,数组下标访问越界,空指针异常、找不到指定类等等
| 创建时间: | 2022/9/12 16:06 |
| 更新时间: | 2022/9/12 16:06 |
| 作者: | Chris |
类的初始化
(1)类什么时候才被初始化
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
(2)类的初始化顺序
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
基于JDK1.8测试可得

有在代码中被调用的Constructor才会被初始化,没有被调用的Constructor不会被初始化
代码中的静态方法如果没有被调用是不会被初始化
| 创建时间: | 2022/9/12 15:27 |
| 更新时间: | 2022/9/12 16:05 |
| 作者: | Chris |

Final variables:
When a variable is declared with final keyword, its value can’t be modified, essentially, a constant. This also means that you must initialize a final variable. If the final variable is a reference, this means that the variable cannot be re-bound to reference another object, but internal state of the object pointed by that reference variable can be changed i.e. you can add or remove elements from final array or final collection. It is good practice to represent final variables in all uppercase, using underscore to separate words.
Initializing a final variable :
We must initialize a final variable, otherwise compiler will throw compile-time error.A final variable can only be initialized once, either via an initializer or an assignment statement. There are three ways to initialize a final variable :
| 创建时间: | 2022/9/11 11:32 |
| 更新时间: | 2022/9/11 11:32 |
| 作者: | Chris |
| 来源: | file:///C:/Users/Dell/Desktop/New%20Microsoft%20Word%20Document.docx |
https://countrycode.org/
https://tool.lu/
https://jsongrid.com/
http://tool.rbtree.cn/regtool/
https://ihateregex.io/
http://jsonpath.com/
https://github.com/Eugeny/tabby/releases/tag/v1.0.183
https://www.zhihu.com/question/64593084
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Encrypt {
private String value;
}
package com.chris.mybatisplus.handler;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.chris.mybatisplus.dto.Encrypt;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 加解密TypeHandler
*/
@MappedJdbcTypes(JdbcType.VARCHAR) //表示处理器处理的Jdbc类型
@MappedTypes(Encrypt.class) //表示该处理器处理的java类型是什么
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {
private static final byte[] KEYS = "12345678abcdefgh".getBytes(StandardCharsets.UTF_8);
/**
* 设置参数
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null || parameter.getValue() == null) {
ps.setString(i, null);
return;
}
AES aes = SecureUtil.aes(KEYS);
String encrypt = aes.encryptHex(parameter.getValue());
ps.setString(i, encrypt);
}
/**
* 获取值
*/
@Override
public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {
return decrypt(rs.getString(columnName));
}
/**
* 获取值
*/
@Override
public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return decrypt(rs.getString(columnIndex));
}
/**
* 获取值
*/
@Override
public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return decrypt(cs.getString(columnIndex));
}
public Encrypt decrypt(String value) {
if (null == value) {
return null;
}
return new Encrypt(SecureUtil.aes(KEYS).decryptStr(value));
}
}
public interface UserMapper extends BaseMapper<User> {
int addUser(@Param("name") String name, @Param("age") Integer age, @Param("bir") Date bir,
@Param("phone") Encrypt phone, @Param("address") String address);
List<User> findUsersByPhone(@Param("phone") Encrypt phone);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chris.mybatisplus.dao.mapper.UserMapper">
<resultMap id="BaseResultMapper" type="com.chris.mybatisplus.entities.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="bir" property="bir"/>
<result column="phone" property="phone"/>
</resultMap>
<insert id="addUser">
insert into t_user(name, age, bir, phone, address)
values (#{name}, #{age}, #{bir}, #{phone}, #{address})
</insert>
<select id="findUsersByPhone" resultMap="BaseResultMapper">
select *
from t_user
where phone = #{phone}
</select>
</mapper>
mybatis-plus:
mapper-locations: classpath:mappers/*.xml
global-config:
db-config:
#驼峰下划线转换
#column-underline: true
#逻辑删除配置
logic-delete-value: I
logic-not-delete-value: A
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#为所有Entity类所在包起默认别名
type-aliases-package: com.chris.mybatisplus.entities
#配置文件中指定Typehandler的包路径
type-handlers-package: com.chris.mybatisplus.handler
package com.chris.mybatisplus;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.chris.mybatisplus.dao.mapper.UserMapper;
import com.chris.mybatisplus.dto.Encrypt;
import com.chris.mybatisplus.entities.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.security.SecureRandom;
import java.util.*;
@SpringBootTest(classes = MybatisPlusMain.class)
public class TestMybaisPlus {
@Autowired
public UserMapper userMapper;
//写入一条记录,并对phone字段加密
@Test
public void testInsertEncrypt() {
int encrypt = userMapper.addUser("cl", 23, new Date(), new Encrypt("1234454556"), "US");
System.out.println("insert user account:" + encrypt);
}
//查询phone对应的用户信息
@Test
public void findUserByPhone() {
List<User> usersByPhone = userMapper.findUsersByPhone(new Encrypt("1234454556"));
System.out.println("usersByPhone:" + JSONUtil.toJsonStr(usersByPhone));
}
}
对方法1中的entity进行改造
public interface UserCryptoMapper extends BaseMapper<UserCrypto> {
int addUser(@Param("name") String name, @Param("age") Integer age, @Param("bir") Date bir,
@Param("phone") Encrypt phone, @Param("address") String address);
List<User> findUsersByPhone(@Param("phone") Encrypt phone);
}
phone字段类型改为Encrypt并加上
typeHandler="com.chris.mybatisplus.handler.EncryptTypeHandler
@Data
@AllArgsConstructor
@Accessors(chain = true)
@NoArgsConstructor
@TableName("t_user") //在不配置的情况下默认表名与类名一致
public class UserCrypto {
public UserCrypto(String name, int age, Date bir) {
this.name = name;
this.age = age;
this.bir = bir;
}
/**
* AUTO:数据库自增
* INPUT:inset前自行设置主键值
* ASSIGN_ID:分配ID,主键类型为Number(Integer和Long)使用接口IdentifierGenerator的nextId方法(默认实现类为DefaultIdentifierGenerator雪花算法)
* ASSIGN_UUID:分配UUID,主键类型为String,使用接口IdentifierGenerator的nextId方法(默认default方法)
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 表中列名与Java类中属性的映射
* 如果列名和属性名一致可以不用配置此注解
*/
@TableField("name")
private String name;
private Integer age;
private Date bir;
private String address;
@TableField(jdbcType = JdbcType.VARCHAR, typeHandler = com.chris.mybatisplus.handler.EncryptTypeHandler.class)
private Encrypt phone;
// 不映射数据表中的任何字段
@TableField(exist = false)
private String notColumn;
}
phone字段加上
typeHandler="com.chris.mybatisplus.handler.EncryptTypeHandler
<mapper namespace="com.chris.mybatisplus.dao.mapper.UserCryptoMapper">
<resultMap type="com.chris.mybatisplus.entities.UserCrypto">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="bir" property="bir"/>
<result column="phone" property="phone" typeHandler="com.chris.mybatisplus.handler.EncryptTypeHandler"/>
</resultMap>
<insert >
insert into t_user(name, age, bir, phone, address)
values (#{name}, #{age}, #{bir}, #{phone}, #{address})
</insert>
<select resultMap="BaseResultMapper">
select *
from t_user
where phone = #{phone}
</select>
</mapper>
@SpringBootTest(classes = MybatisPlusMain.class)
public class EncryptTest {
@Resource
private UserCryptoMapper cryptoMapper;
//写入一条记录,并对phone字段加密
@Test
public void testInsertEncrypt() {
int encrypt = cryptoMapper.addUser("cl", 23, new Date(), new Encrypt("1234454556"), "US");
System.out.println("insert user account:" + encrypt);
}
//查询phone对应的用户信息
@Test
public void findUserByPhone() {
List<User> usersByPhone = cryptoMapper.findUsersByPhone(new Encrypt("1234454556"));
System.out.println("usersByPhone:" + JSONUtil.toJsonStr(usersByPhone));
}
@Test
public void testEncrypt() {
UserCrypto user = new UserCrypto();
user.setAge(21);
user.setBir(new Date());
user.setName("Hency");
user.setPhone(new Encrypt("1263324456677"));
int i = cryptoMapper.insert(user);
System.out.println("insert user account:" + i);
}
}
| 创建时间: | 2022/8/28 13:58 |
| 更新时间: | 2022/8/28 13:58 |
| 作者: | Chris |
https://blog.csdn.net/lmb55/article/details/90380309
| 创建时间: | 2020/11/16 16:29 |
| 更新时间: | 2022/8/18 10:58 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/wangshen31/p/9379197.html |
https://www.cnblogs.com/wangshen31/p/9379197.html
切面所适用的多个场景,包括日志,声明式事务、安全和缓存
切面是通知和切点的结合。
通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能.
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
切点(pointcut)、 通知(advice)和连接点(join point)
切点定义了通知被应用到的具体位置
@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点
@Pointcut("(execution(public * com.chris.demo..*.service..*.*(..))" +
"|| execution(public * com.chris.demo..*.app..*.*(..))" +
"|| execution(public * com.chris.demo..*.api..*.*(..)))" +
"|| execution(public * com.chris.demo..*.web..*Controller.*(..)))")
private void check() {
}
@Pointcut("@annotation(com.chris.demo.common.annotation.LogIgnoreFilter)")
public void ignoreLog() {
}
@Pointcut("(execution(public * com.chris.demo.report.*.*.*(..))" +
"|| execution(public * com.chris.demo.file..*.*.*(..))" +
"|| execution(public * com.chris.demo..*.FileAction.*(..)))")
private void uncheck() {
}
/**
* 定义复合切点
*/
@Pointcut("check() && !uncheck()")
public void serviceLog() {
}
切入点表达式可以用逻辑符号
&&, ||, !来描述
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
- @Before
切入点表达式可以用逻辑符号&&, ||, !来描述
// 在切入点的方法run之前要干的
@Before("check() || uiControllerLog()")
public void logBeforeController(JoinPoint joinPoint) {
- @After
这个注解就是在切入的方法运行完之后把我们的advice增强加进去。
一样方法中可以添加JoinPoint。
- @Around
这个注解可以简单地看作@Before和@After的结合。这个注解和其他的比比较特别,它的方法的参数一定要是ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身
ProceedingJoinPoint有个proceed()方法,相当于就是那切入点的那个方法执行,简单地说就是让目标方法执行,然后这个方法会返回一个对象,这个对象就是那个切入点所在位置的方法所返回的对象。
除了这个Proceed方法(很重要的方法),其他和那几个注解一样。
- @AfterReturning
这个注解可以指定两个属性,
returning属性,表明可以在Advice的方法中有目标方法返回值的形参
@AfterReturning(returning = "returnOb", pointcut = "controllerLog() || uiControllerLog()")
public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
System.out.println("##################### the return of the method is : " + returnOb);
}
- @AfterThrowing
异常抛出增强,在异常抛出后织入的增强。有点像上面的@AfterReturning
这个注解也是有两个属性,pointcut和throwing。
@AfterThrowing(pointcut = "controllerLog() || uiControllerLog()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
}
连接点是程序执行过程中能够应用通知的所有点
JoinPoint : 代表着织入增强处理的连接点。
JoinPoint包含了几个很有用的参数:
Object[] getArgs:返回目标方法的参数
Signature getSignature:返回目标方法的签名
Object getTarget:返回被织入增强处理的目标对象
Object getThis:返回AOP框架为目标对象生成的代理对象
Spring提供了4种类型的AOP支持
前三种都是AOP实现的变体,
Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截
Spring在运行时通过在代理类中包裹切面,把切面织入到Spring管理的bean中。
代理类封装了目标类, 先拦截被通知方法的调用,再把方法调用转发给真正目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

Spring的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法.
在Spring AOP中,要使用AspectJ的
切点表达式语言来定义切点.
Spring AOP仅支持AspectJ切点指示器(pointcut designator)的一个子集。
Spring借助AspectJ的切点表达式语言来定义Spring切面
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常
只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的.
execution(方法修饰符(可选) 返回类型 类路径 方法名 参数 异常模式(可选))
例如:
execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。
对于方法参数列表,我们使用两个(..)表明切点要选择任意的perform()方法,不论该方法的入参是什么。

expression="execution(* com.loongshawn.method.ces..*.*(..))"

1)execution(public * *(..)): 表示匹配所有public方法
2)execution(* set*(..)): 表示所有以“set”开头的方法
3)execution(* com.xyz.service.AccountService.*(..)): 表示匹配所有AccountService接口的方法
4)execution(* com.xyz.service.*.*(..)): 表示匹配service包下所有的方法
5)execution(* com.xyz.service..*.*(..)): 表示匹配service包和它的子包下的方法
Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean.
execution(* concert.Performance.perform()) and bean('woodstock')
在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock
使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能。
使用XML来装配bean的话,那么需要使用Springaop命名空间中的aop:aspectj-autoproxy元素
不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。
我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。
| 创建时间: | 2022/4/6 9:34 |
| 更新时间: | 2022/8/14 18:55 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/taozi32/p/10238568.html |
SpringBoot 检索篇 - 整合Elasticsearch7.6.2
ES 是基于Apache Lucene 构建的开源搜索引擎,Lucene是迄今为止最好的一款搜索引擎工具包,但是Lucene的API相对复杂,很难集成到实际的应用中去。同时ES基于java编写,提供简单易用的RestFul API,开发者可以使用简单的API开发相关搜索功能
计算机程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的位置和次数,当用户查询时再根据建立的索引查找,类似于通过字典查字的过程。

只处理文本,不处理语义
比如 ‘你今年多大了’如果处理语义就会告诉你实际年龄,
如果不处理语意则会把这句话做为搜索关键字来查询出包含它的结果
比数据库查询效率高
搜索结果存在相关度排序
搜索时关键词不区分大小写,mysql只能统一转大写或者统一转小写
ES主要以轻量级的json格式存储数据,同时也支持地理位置查询,也支持地理位置和文本混合查询
在统计,日志数据处理和分析,可视化方面是引领者
wikipedia,stackoverflow,baidu,ali都在使用es做检索
使用比较广泛的平台ELK(ElasticSearch, Logstash, Kibana)
下载 https://www.elastic.co/start
ES默认不能使用root用户启动,创建普通用户es
[root@master home]# groupadd es
[root@master home]# cat /etc/group|grep es
games:x:20:
setroubleshoot:x:987:
pulse-access:x:986:
es:x:1001:
[root@master home]# useradd -m -d /home/es -s /bin/bash -g es es
[root@master home]# passwd es
[root@master home]# id es
uid=1001(es) gid=1001(es) groups=1001(es)
[root@master home]#su es
上传 elasticsearch-7.13.4-linux-x86_64.tar.gz
[root@master opt] wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.13.4-linux-x86_64.tar.gz
[root@master opt]# tar -zxvf elasticsearch-7.13.4-linux-x86_64.tar.gz
[root@master opt]# cd elasticsearch-7.13.4/
[root@master opt]# ll
drwxr-xr-x. 2 root root 4096 Jul 15 02:37 bin
drwxr-xr-x. 3 root root 169 Jul 30 09:52 config
drwxr-xr-x. 9 root root 107 Jul 15 02:37 jdk
drwxr-xr-x. 3 root root 4096 Jul 15 02:37 lib
-rw-r--r--. 1 root root 3860 Jul 15 02:31 LICENSE.txt
drwxr-xr-x. 2 root root 6 Jul 15 02:35 logs
drwxr-xr-x. 59 root root 4096 Jul 15 02:38 modules
-rw-r--r--. 1 root root 594150 Jul 15 02:35 NOTICE.txt
drwxr-xr-x. 2 root root 6 Jul 15 02:35 plugins
-rw-r--r--. 1 root root 2710 Jul 15 02:31 README.asciidoc
bin 可执行脚本目录,启动脚本 elasticsearch
config 存放配置文件目录,核心配置文件 elasticsearch.yml
logs 存放日志目录
plugins 用来扩展ES的插件目录
cd bin/
./elasticsearch
# 后台启动不会占用窗口
./elasticsearch -d

测试es是否启动成功
curl http://localhost:9200
[root@master ~]# curl http://localhost:9200
{
"name" : "master",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "OE2K0TcwT9SIRuEHmI95Tg",
"version" : {
"number" : "7.13.4",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "c5f60e894ca0c61cdbae4f5a686d9f08bcefc942",
"build_date" : "2021-07-14T18:33:36.673943207Z",
"build_snapshot" : false,
"lucene_version" : "8.8.2",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
es最小内存为512m, 不能再小
[root@master config]# vi jvm.options
-Xms512m
-Xmx512m
[es@master bin]$ ps -ef | grep elastic
[es@master bin]$ kill -9 10965
[es@master bin]$ ./elasticsearch
es默认只允许本地访问,如果需要远程访问则需要开启远程访问链接
[es@master config]$ vi elasticsearch.yml
#network.host: 192.168.0.1
修改为:
network.host: 0.0.0.0
ERROR: [3] bootstrap checks failed. You must address the points described in the following [3] lines before starting Elasticsearch.
bootstrap check failure [1] of [3]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]
bootstrap check failure [2] of [3]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
bootstrap check failure [3] of [3]: the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured
切换到root
vi /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 4096
* hard nproc 4096
退出重新登录检查配置是否生效
粘贴以下命令
ulimit -Hn
ulimit -Sn
ulimit -Hu
ulimit -Sn
[root@master ~]# ulimit -Hn
65536
[root@master ~]# ulimit -Sn
65536
[root@master ~]# ulimit -Hu
4096
[root@master ~]# ulimit -Sn
65536
切换到root
在文件中加入如下内容
启动es的用户名 soft nproc 4096
[root@master ~]# vi /etc/security/limits.d/20-nproc.conf
es soft nproc 4096

切换到root
[root@master ~]# vi /etc/sysctl.conf
vm.max_map_count=655360
[root@master ~]# sysctl -p
vm.max_map_count = 655360
[es@master config]$ cd /config/
[es@master config]$ vi elasticsearch.yml
# 修改被注释的如下信息为
node.name: node-1
cluster.initial_master_nodes: ["node-1"]
[root@master config]# firewall-cmd --state
running
[root@master config]# systemctl disable firewalld.service
Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service.
Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
[root@master config]# firewall-cmd --state
running
[root@master config]# systemctl stop firewalld.service
[root@master config]# firewall-cmd --state
not running

从将文档增加到es中到检索文档中的内容全过程仅需要1s
一个索引是一个拥有相似特征的文档的集合,类似于mysql中的database.
用户索引 ,订单索引,商品索引,分类索引
一个索引由一个名字标识,标识必须全为小写字母,在对索引中的文档进行crud时需要用到这个索引名称.
数据库中的database一般是根据应用来创建,但是索引是一组具有相似特征的文档集合,所以实际中一个应用一个database,但可能对应多个索引。

一个类型是索引的一个逻辑上的分类,其语意由个人来定,类型类似于数据库中的表的概念
注意:
将索引比作一个database,将类型比作一个表其实是一个坏的比喻。
在es5.x之前的版本中一个索引可以创建一个或多个类型。
在es6.x版本兼容之前的版本中一个索引创建的一个或多个类型,便是不能在一个索引下再创建多个类型。
在es7.x之后的版本中一个索引仅可以创建一个类型。
官方文档:
Learn -> Docs
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html
类似于关系性数据库中表的schema,用于定义一个索引[index]中类型[type]的数据结构,mapping中包含字段名,字段数据类型和字段索引类型
在默认情况下,es可以根据插入的数据自动地创建type及其mapping。
一个文档是一个可被索引的基础信息单元,类似于关系型数据库中的一条记录。
例如你可以拥有一个员工的文档,也可以拥有某个商品的一个文档
文档采用了轻量级的数据交互格式Json来表示。
ES的客户端工具
当前使用的ES版本与kibana版本一致
上传到es用户的家目录下
[es@master]tar -zxvf kibana-7.13.4-linux-x86_64.tar.gz
[es@master]/home/es/kibana-7.13.4/config
[es@master config]$ vi kibana.yml
server.host: "192.168.80.180"
elasticsearch.hosts: ["http://192.168.80.180:9200"]
kibana 默认端口号为5601
cd bin/
[es@master bin]$ ./kibana
http://master:5601

PUT /ems
结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "ems"
}
GET /_cat/health --查看健康状况
GET /_cat/nodes --查看所有节点
GET /_cat/master --查看主节点
GET _cat/indices --查看所有索引
GET /_cat/indices/v --显示表头查看所有索引
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .apm-custom-link wk57k9ydRfalb-vx5Zon7w 1 0 0 0 208b 208b
green open .kibana-event-log-7.13.4-000001 vf4JjMfEQjGqOlnYz0HQww 1 0 1 0 5.6kb 5.6kb
green open .apm-agent-configuration tCHpjuNmSwq2PiwBadMVeg 1 0 0 0 208b 208b
yellow open ems ysfFkjV5RNOPgNQ68CFK1g 1 1 0 0 208b 208b
green open .kibana_7.13.4_001 HyDQ5FmdRTiQPWjVkqzPsw 1 0 29 5 2.1mb 2.1mb
yellow open dangdang yzm7GHk9RKSA93RA7HzGoQ 1 1 0 0 208b 208b
green open .kibana_task_manager_7.13.4_001 ipMKJ-PaQnWGVYifkpEW-A 1 0 10 1297 243.1kb 243.1kb
health -- 健康状态, GREEN -> YELLOW -> RED
pri --主分片数
rep --副本分片数
docs.count --文档数
docs.deleted --文档是否被删除过
store.size --存储大小
pri.store.size -- 主分片存储大小
GET /dangdang
{
"dangdang" : {
"aliases" : { },
"mappings" : { },
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "dangdang",
"creation_date" : "1627636773007",
"number_of_replicas" : "1",
"uuid" : "yzm7GHk9RKSA93RA7HzGoQ",
"version" : {
"created" : "7130499"
}
}
}
}
}
DELETE /dangdang --删除单个索引
DELETE /* -- 删除所有索引
text, keyword, data, integer, long, double, boolean, ip
PUT /dangdang_2
{
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "keyword"
},
"age":{
"type": "integer"
},
"bir":{
"type": "date"
}
}
}
}
结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "dangdang_2"
}
GET /dangdang2/_mapping
以下命令会自动创建一个索引为tianmao类型为order的文档_id=1
PUT /tianmao/order/1 或者 PUT /tianmao/_doc/1
{
"id":"21",
"name":"chris",
"prvice":23.13,
"amount":23
}
POST /tianmao/order 或者 POST /tianmoa/_doc
{
"id":"12",
"name":"海苔",
"prvice":3.13,
"amount":24
}
-- 会自动生成_id
{
"_index" : "tianmao",
"_type" : "order",
"_id" : "F5I6-noB9Y4OLXrN27h2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
GET /tianmao/order/1
或者 GET/tianmao/_doc/1
{
"_index" : "tianmao",
"_type" : "order",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "21",
"name" : "chris",
"prvice" : 23.13,
"amount" : 23
}
}
新增索引映射
PUT /dangdang/
{
"mappings": {
"properties": {
"name":{"type": "keyword"},
"content":{"type": "text"},
"price":{"type": "float"},
"update":{"type": "date"}
}
}
}
GET /dangdang/_mapping
PUT /dangdang/_doc/1
{
"name": "一只小白羊的故事",
"content":"我是一只小白羊,每天吃草都很忙",
"price":23.4,
"date":"2018-02-23"
}
GET /dangdang/_doc/1
更新之后只剩下name字段, 相当于把原来的文档删除后再添加
POST /dangdang/_doc/1
{
"name":"小白羊多利的故事"
}
GET /dangdang/_doc/1
{
"_index" : "dangdang",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 2,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "小白羊多利的故事"
}
}
POST /dangdang/_doc/1/_update
{
"doc":{
"name":"小白羊多利的故事"
}
}
POST /dangdang/_doc/1/_update
{
"doc":{
"name":"小白羊多利的故事",
"category":"少儿故事类"
}
}
GET /dangdang/_doc/1
{
"_index" : "dangdang",
"_type" : "_doc",
"_id" : "1",
"_version" : 5,
"_seq_no" : 5,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "小白羊多利的故事",
"content" : "我是一只小白羊,每天吃草都很忙",
"price" : 23.4,
"date" : "2018-02-23",
"category" : "少儿故事类"
}
}
GET /dangdang/_mapping
{
"dangdang" : {
"mappings" : {
"properties" : {
"category" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"content" : {
"type" : "text"
},
"date" : {
"type" : "date"
},
"name" : {
"type" : "keyword"
},
"price" : {
"type" : "float"
},
"update" : {
"type" : "date"
}
}
}
}
}
更新price字段,在原来基础上增加1.3
POST /dangdang/_doc/1/_update
{
"script":"ctx._source.price+=1.3"
}
DELETE /dangdang/_doc/1
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
批量更新不会因为其中一条的失败而所有失败
{ "index" : {}} 如是不指定_id则会自动生成_id
POST /dangdang/_doc/_bulk
{ "index" : {"_id" : "11" } }
{ "id" : "11", "name":"c++ dev principles", "price":52.3, "amount":"32"}
{ "index" : {"_id" : "12" } }
{ "id" : "12", "name":"java dev principles", "price":392.3, "amount":"23"}
{ "index" : {}}
{ "id" : "13", "name":"python dev principles", "price":33.3, "amount":"45"}
{ "create" : {"_id" : "14" } }
{ "id" : "14", "name":"devops 实战指南", "price":110.4, "amount":"43"}
{ "delete" : {"_id" : "1" } }
{ "update" : {"_id" : "2" } }
{"doc":{"name":"一只小黑羊的故事-2"}}
es提供了两种数据的检索方式
- 使用url参数方式即query string进行检索
- 使用DSL[domain specified language]方式即request body进行检索
准备数据
PUT /ems/
{
"mappings": {
"properties": {
"name":{"type": "keyword"},
"age":{"type": "integer"},
"bir":{"type": "date"},
"content":{"type": "text"},
"address":{"type": "keyword"}
}
}
}
GET /ems/_mapping
POST /ems/_doc/_bulk
{ "index" :{}}
{ "name":"chris", "age":23, "bir":"2016-12-21", "content":"A search query, or query, is a request for information about data in Elasticsearch data streams or indices.", "address":"China"}
{ "index" : {} }
{ "name":"john", "age":33, "bir":"2018-09-12", "content":"What processes on my server take longer than 500 milliseconds to respond?", "address":"US"}
{ "index" : {}}
{ "name":"petter", "age":13, "bir":"2001-11-09", "content":"A search consists of one or more queries that are combined and sent to Elasticsearch. Documents that match a search’s queries are returned in the hits, or search results, of the response.", "address":"Franch"}
{ "create" : {} }
{ "name":"Ethen", "age":43, "bir":"2019-09-01", "content":"You can use the search API to search and aggregate data stored in Elasticsearch data streams or indices. The API’s query request body parameter accepts queries written in Query DSL.", "address":"China"}
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html
GET /ems/_doc/_search?q=*
GET /ems/_doc/_search?q=*&sort=age:desc&size=2
GET /ems/_doc/_search?q=*&sort=age:desc&size=2
GET ems/_doc/_search
{
"query":{
"match_all":{}
},
"sort":[{
"age":{
"order":"desc"
}
}],
"size":2
}
GET ems/_doc/_search
{
"query":{
"match_all":{}
},
"_source":["name", "content"],
"sort":[{
"age":{
"order":"desc"
}
}],
"size":2
}
term是代表完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇,不会参与ES分词查询
GET ems/_doc/_search
{
"query":{
"term":{
"content":{
"value":"consists"
}
}
}
}
可以支持多个vlaue匹配,只需要一个匹配就可以了
GET ems/_doc/_search
{
"query":{
"terms":{
"content":["consists","query"]
}
}
}
检索年龄大于等13且小于等于33的相关文档
POST /ems/_doc/_search
{
"query":{
"range":{
"age":{
"gte":13,
"lte": 33
}
}
},
"sort":[{
"age":{
"order":"asc"
}
}]
}
检索含有指定前缀的关键字相关文档
使用大写字母不会检索到文档,因为ES在对每一个词建立索引时会将词统一转成小写
POST /ems/_doc/_search
{
"query":{
"prefix":{
"content":{
"value":"or"
}
}
},
"sort":[{
"age":{
"order":"asc"
}
}]
}
? 表未任意一个字符
* 表示零个或多个字符
POST /ems/_doc/_search
{
"query":{
"wildcard":{
"content":{
"value":"*ill*"
}
}
},
"_source":["name", "content"],
"sort":[{
"age":{
"order":"asc"
}
}]
}
值为数组类型,用来获得一组id对应的多个文档
POST /ems/_doc/_search
{
"query":{
"ids":{
"values":["sPWeBHsBjA6v-uGtRIg5", "sfWeBHsBjA6v-uGtRIg5"]
}
},
"_source":["name", "content"],
"sort":[{
"age":{
"order":"asc"
}
}]
}
用来模糊查询含有指定关键字的文档
注意:最大编辑距离为0 1 2
如果关键词的长度为2,则不允许有错误,必须完全配置
如果关键词的长度为3到5,则允许有一个错误
如果关键词的长度为大于5,则最多允许有两个错误
-- 可以查询到name 为Ethan的文档
POST /ems/_doc/_search
{
"query":{
"fuzzy":{
"name":"Ethxxn"
}
}
}
-- 查询不到name 为Ethan的文档,因为不配置尾
POST /ems/_doc/_search
{
"query":{
"fuzzy":{
"name":"Ethxx"
}
}
}
用于组合多条件实现复杂查询
must 相当于 &&
should 相当于 ||
must_not 相当于!
查询elasticsearch关键字同时id不是"sfWeBHsBjA6v-uGtRIg5", "rvWeBHsBjA6v-uGtRIg5"的文档
POST /ems/_doc/_search
{
"query":{
"bool":{
"must":[
{
"term":{
"content":{
"value":"elasticsearch"
}
}
}],
"must_not":[{
"ids":{
"values":["sfWeBHsBjA6v-uGtRIg5", "rvWeBHsBjA6v-uGtRIg5"]
}
}]
}
}
}
在查询过程中会先将查询条件根据当前分词器分词后再进行查询
ES会跟据mapping type中的类型来判断是否需要分词处理,只有text类型会分词处理,其它类型均不分词。
不需要分词的字段会将查询条件进行全匹配
需要分词的字段是将查询条件分词后再逐个匹配
GET /ems/_doc/_search
{
"query":{
"multi_match":{
"query":"query request",
"fields":["name","content"]
}
}
}
在查询过程中会先将查询条件根据当前分词器分词后再进行查询
与multi_match相比,最大的好处就是在查询中可以指定分词器
GET /ems/_doc/_search
{
"query": {
"query_string": {
"query": "query request",
"fields": ["name","content"],
"analyzer":"standard"
}
}
}
GET _analyze
{
"analyzer": "standard", -- simple
"text": ["query request"]
}
原理: 将原始的检索结果与检索关键词的分词作匹配,如果能对应上则对在该词的前后默认加上<em>
多字段查询并对content字段中的查询分词高亮显示
GET /ems/_doc/_search
{
"query":{
"multi_match":{
"query":"query request",
"fields":["name","content"]
}
},
"highlight":{
"fields":{"content":{}}
}
}
自定义高亮显示标签
GET /ems/_doc/_search
{
"query":{
"multi_match":{
"query":"query request",
"fields":["name","content"]
}
},
"highlight":{
"fields":{"content":{}},
"pre_tags":"<span style='color:red'>",
"post_tags":"<span>"
}
}
对所有查询字段高亮显示标签
require_field_match:false
GET /ems/_doc/_search
{
"query":{
"multi_match":{
"query":"query",
"fields":["content"]
}
},
"highlight":{
"fields":{"*":{}},
"pre_tags":"<span style='color:red'>",
"post_tags":"<span>",
"require_field_match":false
}
}
{
"_index" : "ems",
"_type" : "_doc",
"_id" : "svXPBXsBjA6v-uGtDIi2",
"_score" : 0.89701396,
"_source" : {
"name" : "query",
"age" : 39,
"bir" : "2008-12-21",
"content" : "it's operation for query the result your wannt",
"address" : "Australia"
},
"highlight" : {
"name" : [
"<span style='color:red'>query<span>"
],
"content" : [
"it's operation for <span style='color:red'>query<span> the result your wannt"
]
}

创建索引
1.通过扫描数据,按索引中的mapping定义判断是否需要分词
2.对每一个字段中的分词结果在索引区创建索引,索引中包含该词在文章中出现的位置和次数
3.元数据区存储了以_id打头文档的原始数据
检索索引
1.根据关键词定位索引
2.根据索引定位文档
就是将一个文本里面的关键词拆分出来
例如:我是中国人
拆分出: 中国|中国人
去掉停用词和语气词
对英文进行单词分词,对中文进行单字分词。
GET _analyze
{
"analyzer": "standard",
"text":"redis 非常好用 111"
}
{
"tokens" : [
{
"token" : "redis",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "非",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "常",
"start_offset" : 7,
"end_offset" : 8,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "好",
"start_offset" : 8,
"end_offset" : 9,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "用",
"start_offset" : 9,
"end_offset" : 10,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "111",
"start_offset" : 11,
"end_offset" : 14,
"type" : "<NUM>",
"position" : 5
}
]
}
英文单词分词去掉数字,中文不分词
GET _analyze
{
"analyzer": "simple",
"text":"redis 非常好用 111"
}
{
"tokens" : [
{
"token" : "redis",
"start_offset" : 0,
"end_offset" : 5,
"type" : "word",
"position" : 0
},
{
"token" : "非常好用",
"start_offset" : 6,
"end_offset" : 10,
"type" : "word",
"position" : 1
}
]
}
https://github.com/medcl/elasticsearch-analysis-ik
要求使用的ik分词器的版本必须与ES版本严格一致
会把plugin安装在elasticsearch-7.13.4/plugins目录下
cd /home/es/elasticsearch-7.13.4/bin
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-7.13.4.zip
下载zip包: https://github.com/medcl/elasticsearch-analysis-ik/releases
[es@master ~]$ unzip elasticsearch-analysis-ik-7.13.4.zip -d ./elasticsearch-ik
[es@master ~]$ mv elasticsearch-ik/ elasticsearch-7.13.4/plugins/
重启ES
[es@master bin]$ ps -ef | grep elastic
[es@master bin]$ kill -9 10965
[es@master bin]$ ./elasticsearch

ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合,适合 Term Query;
ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”,适合 Phrase 查询。
"content":{"type": "text", "analyzer": "ik_max_word"},
PUT /ems/
{
"mappings": {
"properties": {
"name":{"type": "keyword"},
"age":{"type": "integer"},
"bir":{"type": "date"},
"content":{"type": "text", "analyzer": "ik_max_word"},
"address":{"type": "keyword"}
}
}
}
GET /ems/_mapping
POST /ems/_doc/_bulk
{ "index" :{}}
{ "name":"chris", "age":23, "bir":"2016-12-21", "content":"美国留给伊拉克的是个烂摊子吗.", "address":"China"}
{ "index" : {} }
{ "name":"john", "age":33, "bir":"2018-09-12", "content":"公安部:各地校车将享最高路权", "address":"US"}
{ "index" : {}}
{ "name":"petter", "age":13, "bir":"2001-11-09", "content":"中韩渔警冲突调查:韩警平均每天扣1艘中国渔船", "address":"Franch"}
{ "create" : {} }
{ "name":"Ethen", "age":43, "bir":"2019-09-01", "content":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首", "address":"China"}
POST /ems/_doc/_search
{
"query":{
"term":{
"content":{
"value":"中国"
}
}
}
}
[es@master config]$ vi IKAnalyzer.cfg.xml
-- 修改为:
<entry key="ext_dict">ext.dic</entry>
[es@master config]$ cp extra_main.dic ext.dict
vi ext.dict
碰瓷
李毛毛
[es@master config]$ vi IKAnalyzer.cfg.xml
-- 修改为:
<entry key="ext_stopwords">stopext.dic</entry>
[es@master config]$ cp ext.dic stopext.dict
vi ext.dict
遇到
注意:
# springboot多环境配置
# springboot多环境配置
# 端口,项目上下文
server:
port: 8080
servlet:
context-path: /springboot-params-demo
# 默认启动的是测试环境配置
spring:
profiles:
active: test
# 日志输出配置
logging:
level:
root: INFO
org:
springframework:
security: WARN
web: ERROR
file:
path: ./logs
name: './logs/springboot-params-demo.log'
pattern:
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
# 自定义的参数
myParam: 'on'
# 自定义的参数
myParam: 'close'
获取自定义参数
package com.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Slf4j
public class MyParamRunner implements CommandLineRunner {
/**
* 自定义测试参数
*/
@Value("${myParam}")
String myParam;
/**
* 获取环境变量参数
*/
@Autowired
Environment environment;
@Override
public void run(String... args) throws Exception {
log.info("本地设置的参数myParam为:{}", myParam);
log.info(environment.toString());
log.info(String.valueOf(Arrays.asList(environment.getActiveProfiles())));
}
}
java -jar /usr/local/springboot_demos/springboot-port8001/springboot-jvm-params-1.0-SNAPSHOT.jar

java -jar -Dspring.profiles.active=prod /usr/local/springboot_demos/springboot-port8001/springboot-jvm-params-1.0-SNAPSHOT.jar

java -jar /usr/local/springboot_demos/springboot-port8001/springboot-jvm-params-1.0-SNAPSHOT.jar --spring.profiles.active=prod --myParam='test'

java -jar -DmyParam='test1' /usr/local/springboot_demos/springboot-port8001/springboot-jvm-params-1.0-SNAPSHOT.jar --spring.profiles.active=prod --myParam='test2'

配置文件变量 < JVM系统变量 < 命令行参数(注意:优先级由低到高,非常多的启动命令中传参也是这个道理)
/usr/local/jdk/jdk1.8.0_261/bin/java -jar -server \ ## 服务模式,linux默认是server模式,window默认是client参数
-XX:+HeapDumpOnOutOfMemoryError \ ## 当OOM发生时自动生成Heap Dump文件
-XX:HeapDumpPath=/usr/local/springboot_demos/springboot-port8001/dump/heap/oom.hprof \ ## 指定发生OOM时生成Dump文件存储位置
-Djava.io.tmpdir=/usr/local/springboot_demos/springboot-port8001/tmp/ \ ## 指定操作系统缓存的临时目录
-Dserver.port=8001 \ ## web服务使用端口
-Dcom.sun.management.jmxremote \ ## 是否支持远程JMX访问,默认true
-Dcom.sun.management.jmxremote.port=5103 \ ## 配置jmx远程connection的端口号,要确认这个端口没有被占用
-Dcom.sun.management.jmxremote.rmi.port=6103 \ ## JMX在远程连接时,会随机开启一个RMI端口作为连接的数据端口
-Dcom.sun.management.jmxremote.authenticate=false \ ## 是否需要开启用户认证,默认开启
-Dcom.sun.management.jmxremote.ssl=false \ ## 是否连接开启SSL加密,默认开启
-Dcom.sun.management.jmxremote.access.file=/usr/local/jdk/jdk1.8.0_261/jre/lib/management/jmxremote.access \ 对访问用户的权限授权的文件的路径,默认路径是${
JRE_HOME}/lib/management/jmxremote.access
-Xmx256m \ ## 设置堆最大空间为256m
-Xms256m \ ## 设置堆最小空间为256m
-XX:+DisableExplicitGC \ ## 禁止手动的system.gc
-Xloggc:/usr/local/springboot_demos/springboot-port8001/logs/springboot-jvm-params_gc.%t.log \ gc日志存放的位置
-XX:+PrintHeapAtGC \ ## HotSpot在GC前后都会将GC堆的概要状况输出到log中
-XX:+PrintTenuringDistribution \ ## 打印Survivor对象年龄分布
-XX:+PrintGCApplicationStoppedTime \ ## 预估垃圾收集"Stop the world"暂停所阻塞的时间
-XX:+PrintGCTaskTimeStamps \ ## 打印gc线程的时间戳
-XX:+PrintGCDetails \ ## 打印gc详情
-XX:+PrintGCDateStamps \ ## 日志开头显示日期以及时间
-Dserver.connection-timeout=60000 \ ## HTTP请求超时时间
-Dserver.tomcat.accept-count=1000 \ ## 所有可能的请求处理线程正在使用时,传入连接请求的最大队列长度
-Dserver.tomcat.max-threads=300 \ ## 最大工作线程数
-Dserver.tomcat.min-spare-threads=65 \ ## 最小工作线程数
-Dserver.tomcat.accesslog.enabled=false \ ## 启用访问你日志
-Dserver.tomcat.accesslog.directory=/usr/local/springboot_demos/springboot-port8001/logs/ \ ## 日志文件路径
-Dserver.tomcat.accesslog.prefix=access_log \ ## 日志文件名前缀
-Dserver.tomcat.accesslog.pattern=combined \ ## 日志格式
-Dserver.tomcat.accesslog.suffix=.log \ ## 日志文件后缀
-Dserver.tomcat.accesslog.file-date-format=.yyyy-MM-dd ## 放在日志文件名中的日期格式
-Dserver.tomcat.accesslog.rotate=true \ ## 是否启用访问日志分割
-Dserver.tomcat.accesslog.rename-on-rotate=true \ ## 推迟在文件名中加入日期表示,直到日志分割时
-Dserver.tomcat.accesslog.request-attributes-enabled=true \ ## 为请求使用的IP地址、主机名、协议和端口设置请求属性
-Dserver.tomcat.accesslog.buffered=true \ ## 缓存日志定期刷新输出(建议设置为true,否则当有请求立即打印日志对服务的响应会有影响)
-XX:NewRatio=4 \ ## 设置Yang和Old的比例,设置4则Old是Yang的4倍,即Yang占1/5
-XX:SurvivorRatio=8 \ ## 设置Eden和Suivior的比例,Eden:S0:S1=8:1:1
-XX:MaxTenuringThreshold=15 \ ## 在新生代对象存活次数(经过Minor GC的次数)超过n后,就会晋升到老年代
-XX:TargetSurvivorRatio=90 \ ## 在新生代的对象不一定要满足存活年龄达到MaxTenuringThreshold才能去老年代,当Survivor空间中相同年龄所有对象大小总和大于[Desired survivor size]时,年龄大于或等于该年龄的对象直接进入老年代。[Desired survivor size]=单个survivor大小*TargetSurvivorRatio百分比
-XX:+UseCMSInitiatingOccupancyOnly \ 指在使用CMS收集器的情况下,老年代使用了指定阈值的内存时,触发FullGC
-XX:CMSInitiatingOccupancyFraction=70 \ ## 指在使用CMS收集器的情况下,老年代使用达到70%,出发CMS垃圾回收
-XX:ParallelGCThreads=8 \ ## parallel回收的时候可以设置年轻代的并行线程数,取决于cpu核数
-XX:ConcGCThreads=2 \ ## 设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的 1/4 左右。
-XX:-UseGCOverheadLimit \ ## jvm gc行为中超过98%以上的时间去释放小于2%的堆空间时会报“GC overhead limit exceeded”错误,此参数避免此报错
-XX:+UseParNewGC \ ## 开启此参数使用ParNew & serial old搜集器(不推荐)使用这个参数后会在新生代进行并行回收
-XX:+UseConcMarkSweepGC \ ## 开启此参数使用ParNew & CMS(serial old为替补)搜集器
-XX:CMSFullGCsBeforeCompaction=1 \ ## 设置在几次CMS垃圾收集后,触发一次内存整理
-XX:+CMSParallelRemarkEnabled \ ## 降低标记停顿
-XX:+CMSScavengeBeforeRemark \ ## 开启或关闭在 CMS-remark 阶段之前的清除(Young GC)尝试
-XX:+ParallelRefProcEnabled \ ## 并行处理Reference,加快处理速度,缩短耗时
-XX:+UseCMSCompactAtFullCollection \ ## 年老代使用CMS,默认是不会整理堆碎片的。设置此配置打开对年老代的压缩,即执行Full GC后对内存进行整理压缩,免得产生内存碎片,但有可能会影响性能。
-XX:CMSMaxAbortablePrecleanTime=6000 \ ## 指定CMS-concurrent-abortable-preclean阶段执行的时间,该阶段主要是执行一些预清理,减少应用暂停的时间
-XX:CompileThreshold=10 \ ## 超过10此进行JTI即时编译
-XX:MaxInlineSize=1024 \ ## 方法体的大小阈值。通过 -XX:CompileThreshold 来设置热点方法的阈值。但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作
-Dsun.net.client.defaultConnectTimeout=60000 \ ## socket连接超时时间
-Dsun.net.client.defaultReadTimeout=60000 \ ## socket读取超时时间
-Dnetworkaddress.cache.ttl=300 \ ## JVM的DNS缓存有效期,单位秒
-Dsun.net.inetaddr.ttl=300 \ ## 缓存失败结果,如果在缓存时效内再次lookup时直接返回错误(减轻DNS服务压力)
-Djsse.enableCBCProtection=false \ ## 关闭jvm中的java修复程序
-Djava.security.egd=file:/dev/./urandom \ ## 加快随机数产生过程
-Dfile.encoding=UTF-8 \ ## 指定web应用编码
-Dlog.path=/usr/local/springboot_demos/springboot-port8001/logs/ \ ## 指定项目日志文件路径
-Dspring.profiles.active=prod \ ## 指定运行的环境配置
/usr/local/springboot_demos/springboot-port8001/springboot-jvm-params-1.0-SNAPSHOT.jar jvmparams
| 创建时间: | 2022/5/23 10:22 |
| 更新时间: | 2022/5/23 11:23 |
| 来源: | https://mp.weixin.qq.com/s/0qK25ywuCquVplPT2WcPmA |
| 创建时间: | 2022/5/23 6:27 |
| 来源: | https://mp.weixin.qq.com/s/5lNoImqKhzr96n7Ny2DIbg |
| 创建时间: | 2022/5/20 15:07 |
| 更新时间: | 2022/5/20 18:05 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/lwg_1540652358/article/details/84111339 |
https://gitcode.net/mirrors/json-path/jsonpath?utm_source=csdn_github_accelerator
| 操作 | 说明 |
|---|---|
| $ | 查询的根节点对象,用于表示一个jsono数据,可以是数组或对象 |
| @ | 处理当前节点对象类似于java中的this |
| * | 通配符,必要时可用任何地方的名称或数字 |
| .. | 深层扫描。可以理解为递归搜索 |
| .<name> | 表示一个子节点 |
| ['<name>' (, '<name>')] | 括号表示子项 |
| [<number> (, <number>) ] | 表示一个或多个数组下标 |
| [start:end] | 数组切片操作,区间为 [start,end) , 不包含end |
| [?(<expression>)] | 过滤表达式。 表达式必须求值为一个布尔值 |
过滤器是用于筛选数组的逻辑表达式。一个典型的过滤器将是[?(@.age > 18)],其中@表示正在处理的当前项目。
可以使用逻辑运算符&&和||创建更复杂的过滤器。
字符串文字必须用单引号或双引号括起来([?(@.color == 'blue')] 或者 [?(@.color == "blue")])
| 操作符 | 描述 |
|---|---|
| == | left等于right(注意1不等于'1') |
| != | 不等于 |
| < | 小于 |
| <= | 小于等于 |
| > | 大于 |
| >= | 大于等于 |
| =~ | 匹配正则表达式[?(@.name =~ /foo.*?/i)] |
| in | 左边存在于右边 [?(@.size in ['S', 'M'])] |
| nin | 左边不存在于右边 |
| size | (数组或字符串)长度 |
| empty | (数组或字符串)为空 |
{
"store": {
"book": [{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}, {
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}, {
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}, {
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
| 操作 | 说明 |
|---|---|
| $.store.book[*].author | The authors of all books |
| $..author | All authors |
| $.store.* | All things, both books and bicycles |
| $.store..price | The price of everything |
| $..book[2] | The third book |
| $..book[-2] | The second to last book |
| $..book[0,1] or $..book[:2] | The first two books |
| $..book[:2] | All books from index 0 (inclusive) until index 2 (exclusive) |
| $..book[1:2] | All books from index 1 (inclusive) until index 2 (exclusive) |
| $..book[(@.length-2)] or $..book[-2:] | Last two books |
| $..book[2:] | Book number two from tail |
| $..book[?(@.isbn)] | All books with an ISBN number |
| $.store.book[?(@.price < 10)] | All books in store cheaper than 10 |
| $..book[?(@.price <= $['expensive'])] | All books in store that are not "expensive" |
| $..book[?(@.author =~ /.*REES/i)] | All books matching regex (ignore case) |
| $..* | Give me every thing |
| $..book.length() | The number of books |
| 创建时间: | 2022/4/14 14:41 |
| 更新时间: | 2022/5/19 15:40 |
| 作者: | Chris |
org.springframework.context.annotation.Condition
org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
| 创建时间: | 2022/5/14 10:45 |
| 更新时间: | 2022/5/14 11:32 |
| 作者: | Chris |
http://so.baiduyun.me/
http://manybooks.net/
http://book.zi5.me/
https://libsolutions.net/booklist/419507/d51dd4
zh.singlelogin.app
zh.singlelogin.me
zh.zlib.life
zh.1lib.world
zh.libsolutions.domains
| 创建时间: | 2022/3/28 15:17 |
| 更新时间: | 2023/3/6 22:00 |
| 作者: | Chris |
| 来源: | https://mp.weixin.qq.com/s?__biz=MzI5MTkxMDU2MQ==&mid=2247483895&idx=2&sn=9acfb895f94daaaf45a530f12e11bb0b&chksm=ec082739db7fae2f977773fd1eb8afac639243805b3785cb623dcd5a82f0476d9e6d4d0116f8&scene=27 |
1. arthas idea
2. Extra Icons
3. Free Mybatis plugin
4. Lombok
5. Maven Helper
6. Rainbow Brackets
7. Mybatis Log Plugin
8. AiXcoder Code Completer
9. CodeGlance 显示代码缩略图插件
| 创建时间: | 2022/5/12 21:22 |
| 来源: | https://mp.weixin.qq.com/s/d5easnc7Tnhkp1aeKVAteg |
| 创建时间: | 2022/3/30 23:43 |
| 更新时间: | 2022/5/10 18:57 |
| 来源: | https://mp.weixin.qq.com/s/YmRFsFVMeqmNNm5sQCBKSg |


@ConditionalOnProperty注解可以结合配置文件来实现唯一注入。下面示例就是说如果配置文件中配置了lonely.wolf=test1,那么就会将 Wolf1Bean 初始化到容器,此时因为其他实现类不满足条件,所以不会被初始化到 IOC 容器,所以就可以正常注入接口:@Resource注解的形式动态指定 BeanName 来获取:@Primary注解来表示当有多个 Bean 满足条件时,优先注入当前带有@Primary注解的 Bean:ApplicationContext对象,这时候可以通过以下 5 种方式进行获取:ApplicationContext对象,然后就可以通过ApplicationContext对象获取 Bean :ApplicationContextAware接口来获取ApplicationContext对象,从而获取 Bean。需要注意的是,实现ApplicationContextAware接口的类也需要加上注解,以便交给 Spring 统一管理(这种方式也是项目中使用比较多的一种方式):WebApplicationObjectSupport继承了ApplicationObjectSupport,所以并无实质的区别。HttpServletRequest对象,再结合 Spring 自身提供的工具类WebApplicationContextUtils也可以获取到ApplicationContext对象,而HttpServletRequest对象可以主动获取(如下 getBean2 方法),也可以被动获取(如下 getBean1 方法):@Autowrite,也可以通过@Resource注解来注入,这两个注解有什么区别呢?@Autowrite:通过类型去注入,可以用于构造器和参数注入。当我们注入接口时,其所有的实现类都属于同一个类型,所以就没办法知道选择哪一个实现类来注入。@Resource:默认通过名字注入,不能用于构造器和参数注入。如果通过名字找不到唯一的 Bean,则会通过类型去查找。如下可以通过指定 name 或者 type 来确定唯一的实现:@Qualifier注解是用来标识合格者,当@Autowrite和@Qualifier一起使用时,就相当于是通过名字来确定唯一:@Resource就好了,何必用两个注解结合那么麻烦,这么一说似乎显得 @Qualifier 注解有点多余?Bean (MyElement),而且方法中的参数又有 Wolf1Bean 对象,那么这时候 Spring 会帮我们自动注入 Wolf1Bean:@Resource注解又是不能用在参数中,所以这时候就需要使用@Qualifier注解来确认唯一实现了(比如在配置多数据源的时候就经常使用@Qualifier注解来实现):
| 创建时间: | 2020/11/2 10:05 |
| 更新时间: | 2022/5/6 19:31 |
| 作者: | Chris |
| 来源: | http://loadhtml/ |
速度更快
hashmap底层用红黑树算法实现,让查询等操作更快
Lamble表达式
Stream API
便于并行
Stream API声明性的通过parallel() 和sequencial() 在并行流与顺序流之间进行切换
最大化的减少空指针
可以通过Optional来配置一个默认的对象来帮我们减少空指针
新的语法,是一个匿名函数,可以将代码像参数一样进行传递
@Test
public void test2() {
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
int result1 = comparator.compare(12, 34);
System.out.println(result1);
//Lambda表达式
Comparator<Integer> comparator2 = (o1, o2) -> Integer.compare(o1, o2);
int result2 = comparator2.compare(12, 2);
System.out.println(result2);
//方法引用
Comparator<Integer> comparator3 = Integer::compare;
int result3 = comparator3.compare(12, 12);
System.out.println(result3);
}
Lambda表达式的本质就是作为函数式接口的实例
-> Lambda 操作符,
左边为形参列表,其实就是是原来方法中的形参列表;
1. 类型推断,参数的类型可以省略
2. 无参无返回值,小括号不可以省略
3. 如果只有一个参数,小括号可以省略
4. 有两个及以上参数, 小括号不可以省略
右边为Labmda体,其实就是重写的方法的方法体
1. 多条执行语句,并且有返回值要有return和{}
2. 只有一条语句, return与{}若有都可以省略
@Test
public void test1() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hellow runnable r1.");
}
};
r1.run();
// Lambda表达式做为左边接口的一个实例
Runnable r2 = () -> System.out.println("hellow runnable r2.");
r2.run();
}
/**
* 有一个参数无返回值
*/
@Test
public void test2() {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("谎言和誓言的区别是什么?");
Consumer<String> consumer1 = (String s) -> System.out.println(s);
consumer2.accept("一个是听的人当真了,一个是说的人当真了");
//如果只有一个参数,小括号可以省略
Consumer<String> consumer3 = s -> System.out.println(s);
consumer3.accept("一个是听的人当真了,一个是说的人当真了");
}
//类型推断,参数的类型可以省略
Consumer<String> consumer2 = (s) -> System.out.println(s);
consumer2.accept("一个是听的人当真了,一个是说的人当真了");
有两个及以上参数,小括号不可以省略
有多条执行语句,并且有返回值
Comparator<Integer> comparator2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return Integer.compare(o1, o2);
};
int result2 = comparator2.compare(12, 2);
System.out.println(result2);
Comparator<Integer> comparator3 = (o1, o2) -> o1.compareTo(o2);
int result3 = comparator3.compare(12, 12);
System.out.println(result3);
如果一个接口中只有一个抽象方法,则此接口称为函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
消费型 Consumer<T> void accept(T t)
供给型 Supplier<T> T get()
函数型 Function<T, R> R apply(T t)
断定型 Predicate<T> boolean test(T t)
public class LambdaTest2 {
public void happTime(int money, Consumer<Integer> cost) {
cost.accept(money);
}
@Test
public void test1() {
happTime(500, new Consumer<Integer>() {
@Override
public void accept(Integer cost) {
System.out.println("the cost is :" + cost);
}
});
//Lambda表达式
happTime(400, money -> System.out.println("the cost is :" + money));
}
/**
* 根据给定的规则过滤集合中的字符串,此规则由predicate的方法决定
*/
public List<String> fileterStr(List<String> list, Predicate<String> pre) {
List<String> afterFiltering = new ArrayList<>();
for (String s : list) {
if (pre.test(s)) {
afterFiltering.add(s);
}
}
return afterFiltering;
}
@Test
public void test2() {
List<String> list = Arrays.asList("Chris", "John", "Stephone", "Hedy");
List<String> fileteredStr = fileterStr(list, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("s");
}
});
System.out.println(fileteredStr);
//Lambda表达式
List<String> fileteredStr2 = fileterStr(list, s -> s.contains("s"));
System.out.println(fileteredStr2);
}
}
StreamAPI把真正的函数式编程风格引入了JAVA,StreamAPI提供了极高的生产力,可以让我们的代码更加高效,干净和简洁
StreamAPI 提供了高效且易于使用的处理数据的方式
使用StreamAPI 可以对集合进行复杂的查找,过滤和映射操作,也可以使用StreamAPI 并行的来执行操作。
Collection 是一种静态的内存数据结构
Stream 是有关数据的处理算法,是面向CPU的算法方式
- 不存储数据,
- 不会改变源对象,每次都会返回一个持有结果的新的Stream
- Steam操作是延迟执行的,会等到需要结果时再执行,即transform时不执行,action时才执行
- 创建
- 中间操作 Transform
- 终止操作 Action
public void createStreamByCollection() {
List<Employee> employees = EmployeeData.getEmployees();
//返回一个顺序流
Stream<Employee> stream = employees.stream();
//返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}
public void createStreamByArrays() {
int[] intArr = new int[]{1, 2, 3, 4, 5, 6};
IntStream stream = Arrays.stream(intArr);
Employee chris = new Employee(1, "Chris", 23, 23000.43);
Employee john = new Employee(2, "John", 14, 3000.43);
Employee[] empArr = new Employee[]{chris, john};
Stream<Employee> empStream = Arrays.stream(empArr);
}
public void createStreamByStream() {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
public void createStreamByStream2() {
/*
迭代
创建并遍历前10个偶数
*/
Stream.iterate(0, seed -> seed + 2).limit(10).forEach(System.out::println);
/*
生成
创建并遍历前10个偶数
*/
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}



终止操作可以从流中生成结果,其结果可以是任何非流的数据
流进行了终止操作后,不能两次使用


@Test
public void testReduce() {
//计算1到10自然数之和
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
//计算公司中所有员工的工资总和
List<Employee> employees = EmployeeData.getEmployees();
Stream<Double> doubleStream = employees.stream().map(Employee::getSalary);
// Optional<Double> reduce= doubleStream.reduce((a, b) -> a + b);
Optional<Double> reduce = doubleStream.reduce(Double::sum);
Double totalSalary = reduce.orElse((double) 0);
System.out.println(totalSalary);
}

@Test
public void testCollector() {
List<Employee> employees = EmployeeData.getEmployees();
List<Employee> list = employees.stream().filter(e -> e.getSalary() > 10000).collect(Collectors.toList());
Set<Employee> set = employees.stream().filter(e -> e.getSalary() > 10000).collect(Collectors.toSet());
List<Employee> list2 =
employees.stream().filter(e -> e.getSalary() > 10000).collect(Collectors.toCollection(ArrayList::new));
LinkedHashMap<Integer, List<Employee>> ageGrouping =
employees.stream().collect(Collectors.groupingBy(Employee::getAge, LinkedHashMap::new, Collectors.toList()));
Map<Integer, Employee> ageMaping = employees.stream().collect(Collectors.toMap(Employee::getAge, e -> e,
(oldKey, newKey) -> newKey));
}
Optional<T> 是一个容器类,它可以保存类型T的值,代表这个值存在,或者仅仅保存为null, 表示这个值不存在。
原来用null表示一个值不存在,现在Optional可以更好的表达这个概念。并且如以避免空指针异常。
如果值存在则 isPresent() 方法返回true,调用get() 方法会返回该对象。
Optional.of(T t) 创建一个Optional实例,t为非空对象
Optional.empty() 创建一个空的Optional实例
Optional.ofNullable(T t) t可以为空对象

boolean isPresent() 判断是否包含对象
void ifPresent(Consumer<? super T> consumer): 如果有值,就执行Consumer接口实现的代码,并且该值会作为参数传给它。
T get() 如果调用对象包含值,返回该值,否则抛出异常
T orElse(T other) 如果有值则返回,否则返回指定的other对象
T orElseGet(Supplier<? extends T> other) 如果有值则返回,否则返回Supplier接口
实现的对象
T orElseThrow(Supplier<? extends X> exceptionSupplier) 如果有值则返回,否则返回Supplier接口实现的异常
注意
User user = null;
user = Optional.ofNullable(user).orElse(createUser());
user = Optional.ofNullable(user).orElseGet(() -> createUser());
public User createUser(){
User user = new User();
user.setName("zhangsan");
return user;
}
orElse(T other)和orElseGet(Supplier<? extends T> other)的区别:
当user不为null时,orElse函数依然会执行createUser方法,但会将原user值返回,
而orElseGet函数并不会执行createUser()方法,
map(Function mapper) 和 flatMap(Function> mapper)
这两个函数,在函数体上没什么区别。
唯一区别的就是入参,
map函数所接受的入参类型为Function<? super T, ? extends U>,
而flapMap的入参类型为Function<? super T, Optional<U>>。
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
public class User {
private String name;
public String getName() { return name; }
}
// 这时候取name的写法如下所示
String city = Optional.ofNullable(user).map(u-> u.getName()).get();
public class User {
private String name;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
}
// 这时候取name的写法如下所示
String city = Optional.ofNullable(user).flatMap(u-> u.getName()).get();
isPresent即判断value值是否为空,而ifPresent就是在value值不为空时执行consumer
public User getUser(User user) throws Exception{
if(user!=null){
String name = user.getName();
if("zhangsan".equals(name)){
return user;
}
}else{
user = new User();
user.setName("zhangsan");
return user;
}
}
// java8写法
public User getUser(User user) {
return Optional.ofNullable(user)
.filter(u->"zhangsan".equals(u.getName()))
.orElseGet(()-> {
User user1 = new User();
user1.setName("zhangsan");
return user1;
});
}
public String getCity(User user) throws Exception{
if(user!=null){
if(user.getAddress()!=null){
Address address = user.getAddress();
if(address.getCity()!=null){
return address.getCity();
}
}
}
throw new Excpetion("取值错误");
}
// java8写法
public String getCity(User user) throws Exception{
return Optional.ofNullable(user)
.map(u-> u.getAddress())
.map(a->a.getCity())
.orElseThrow(()->new Exception("取指错误"));
}
List<Integer> nums = Lists.newArrayList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
System.out.println("求和:" + nums.stream()// 转成Stream
.filter(team -> team != null)// 过滤
.distinct()// 去重
.mapToInt(num -> num * 2)// map操作
.skip(2)// 跳过前2个元素
.limit(4)// 限制取前4个元素
.peek(System.out::println)// 流式处理对象函数
.sum());//
Map<String, List<Long>> actNameMap = activities.stream()
.filter(activity -> !Objects.equals(activity.getName(), ProcessActivityEnum.CountersignForEn.getActDisplay()))
.collect(Collectors.groupingBy(GimProcessActivity::getName, Collectors.mapping(GimProcessActivity::getId, Collectors.toList())));
List<PlanCountryProviderConfigBean> uniqueCaEndc_Configs = configLists.stream()
.filter(configBean -> !isAllDel(configBean))
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(PlanCountryProviderConfigBean::getCaEndc))),
ArrayList::new));
private <T> Predicate<T> distinctByProperties(String[] properties) {
List<String[]> list = CollUtil.newLinkedList();
return t -> {
String[] t_values = buildValues(t, properties);
if (CollUtil.isEmpty(list)) {
list.add(t_values);
return true;
}
int i = 0;
for (; i < list.size(); i++) {
String[] values = list.get(i);
int j = 0;
for (; j < values.length; j++) {
String t_value = t_values[j];
String value = values[j];
if (Objects.nonNull(t_value) && Objects.nonNull(value) && !t_value.equals(value)) {
break;
}
}
if (j == values.length) {
return false;
}
}
if (i == list.size()) {
list.add(t_values);
}
return true;
};
}
private <T> String[] buildValues(T obj, String[] properties) {
String[] values = new String[properties.length];
Class<?> clz = obj.getClass();
for (int i = 0; i < properties.length; i++) {
Field field;
String property = properties[i];
try {
field = clz.getDeclaredField(property);
field.setAccessible(true);
Object value = field.get(obj);
values[i] = String.valueOf(value);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("error happened when get value of field:" + property + ", from:" + obj, e);
}
}
return values;
}
| 创建时间: | 2022/5/2 12:10 |
| 更新时间: | 2023/3/19 21:07 |
| 作者: | Chris |
代码
https://github.com/mxg133/learnforJava_DesignPattern
相同功能的代码不用重复编写
编程的规范性,便于其它人员阅读和理解
新增功能时非常的方便
当新增或减少功能后,对原有的功能不会产生影响
所有以上特性都是为了让我们的代码呈现高内聚,低耦合
即一个类只负责一个职责
若一个类负责不同的职责,当职责1的需求变化而改变类时,可能造成职责2的错误执行,所以需要将类拆分为类1和类2
降低类的复杂性和变更引起的风险,提高可读性和可扩展性
如果逻辑足够简单可以在方法级别实现单一职责原则
一个类对另一个类的依赖应该建立在
最小的接口上
这个接口里面用不到的方法,应该把大的接口拆分成小的接口,然后依赖小的接口
类A通过接口1,2依赖类B
类C通过接口1,3依赖类C

public class Segregation1 {
public static void main(String[] args) {
A a = new A();
a.depend1(new B()); // A类通过接口去依赖B类
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D()); // C类通过接口去依赖(使用)D类
c.depend4(new D());
c.depend5(new D());
}
}
// 接口1
interface Interface1 {
void operation1();
}
// 接口2
interface Interface2 {
void operation2();
void operation3();
}
// 接口3
interface Interface3 {
void operation4();
void operation5();
}
class B implements Interface1, Interface2 {
public void operation1() {
System.out.println("B 实现了 operation1");
}
public void operation2() {
System.out.println("B 实现了 operation2");
}
public void operation3() {
System.out.println("B 实现了 operation3");
}
}
class D implements Interface1, Interface3 {
public void operation1() {
System.out.println("D 实现了 operation1");
}
public void operation4() {
System.out.println("D 实现了 operation4");
}
public void operation5() {
System.out.println("D 实现了 operation5");
}
}
// A类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
class A {
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface2 i) {
i.operation2();
}
public void depend3(Interface2 i) {
i.operation3();
}
}
// C类通过接口Interface1,Interface3 依赖(使用) D类,但是只会用到1,4,5方法
class C {
public void depend1(Interface1 i) {
i.operation1();
}
public void depend4(Interface3 i) {
i.operation4();
}
public void depend5(Interface3 i) {
i.operation5();
}
}
- 核心思想就是依赖接口编程
- 高层模块不应该依赖低层模块,二者应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
- 细节是多变的,抽象是稳定的,所以抽象基础搭建的框架要比以细节为基础搭建的框架稳定的多。
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,具体的细节由实现类负责完成。
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
/*
完成Person接收消息的功能
方式1分析
1. 简单,比较容易想到
2. 如果我们获取的对象是 weixin,短信等等,则新增类,同时Perons也要增加相应的接收方法
3. 解决思路:
引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖
因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok, 这样我们就符号依赖倒转原则
*/
class Person {
public void receive(Email email ) {
System.out.println(email.getInfo());
}
}
改进后的方法
public class DependecyInversion {
public static void main(String[] args) {
//客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
//定义接口
interface IReceiver {
public String getInfo();
}
class Email implements IReceiver {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//增加微信
class WeiXin implements IReceiver {
public String getInfo() {
return "微信信息: hello,ok";
}
}
//方式2
//同时也遵循了OCP,对使用方修改关闭,只需要增加消息提供方,即对提供方扩展开放
class Person {
//这里我们是对接口的依赖
public void receive(IReceiver receiver) {
System.out.println(receiver.getInfo());
}
}
// 方式1: 通过接口传递实现依赖
// 开关的接口
interface IOpenAndClose1 {
void open1(ITV1 tv); //抽象方法,接收接口
}
interface ITV1 { //ITV接口
void play1();
}
class ChangHong1 implements ITV1 {
@Override
public void play1() {
// TODO Auto-generated method stub
System.out.println("长虹电视机,打开");
}
}
// 实现接口
class OpenAndClose1 implements IOpenAndClose1 {
public void open1(ITV1 tv) {
tv.play1();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ChangHong changHong = new ChangHong();
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.open(changHong);
}
// 方式2: 通过构造方法依赖传递
interface IOpenAndClose2 {
void open2(); //抽象方法
}
interface ITV2 { //ITV接口
void play2();
}
class ChangHong2 implements ITV2 {
@Override
public void play2() {
// TODO Auto-generated method stub
System.out.println("长虹电视机,打开");
}
}
class OpenAndClose2 implements IOpenAndClose2 {
public ITV2 tv; //成员
public OpenAndClose2(ITV2 tv) {
//构造器
this.tv = tv;
}
public void open2() {
this.tv.play2();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ChangHong2 changHong = new ChangHong2();
//通过构造器进行依赖传递
OpenAndClose2 openAndClose = new OpenAndClose2(changHong);
openAndClose.open2();
}
// 方式3 , 通过setter方法传递
interface IOpenAndClose {
void open(); // 抽象方法
void setTv(ITV tv);
}
interface ITV { // ITV接口
void play();
}
class OpenAndClose implements IOpenAndClose {
private ITV tv;
public void setTv(ITV tv) {
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
class ChangHong implements ITV {
@Override
public void play() {
// TODO Auto-generated method stub
System.out.println("长虹电视机,打开");
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ChangHong changHong = new ChangHong();
//通过setter方法进行依赖传递
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.setTv(changHong);
openAndClose.open();
}
- 低层模块尽量要有抽象类或接口, 或者两个都有,这样稳定性会更好
- 变量的声明类型尽量是抽象类或接口,这样我们变量的引用和实际对象间,就存在一个
缓存层,利于扩展和优化。- 继承时遵循里氏替换原则
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定
规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。- 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来
侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障- 问题提出:在编程中,如何正确的使用继承? =>
里氏替换原则
里氏替换原则是在告诉我们继承需要注意什么问题以及要遵守什么规则
- 所有引用父类的地方可以透明的使用其子类
- 在使用继承时,在子类中尽量不要重写父类的方法,特别是运行
多态比较频繁的时候。- 继承实际上是增加类之间的耦合性,通用的做法是:原来的父类和子类都继承一个更通用的基类, 在适当的情况下可以通过
聚合,组合,依赖来解决问题。

public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出11-3
System.out.println("1-8=" + b.func1(1, 8));// 1-8
System.out.println("11+3+9=" + b.func2(11, 3))
}
}
// A类
class A {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
/*
B类继承了A
增加了一个新功能:完成两个数相加,然后和9求和
*/
class B extends A {
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
}
改进后的代码
public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
//因为B类不再继承A类,因此调用者,不会再func1是求减法
//调用完成的功能就会很明确
System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3
System.out.println("1+8=" + b.func1(1, 8));// 1+8
System.out.println("11+3+9=" + b.func2(11, 3));
//使用组合仍然可以使用到A类相关方法
System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3
}
}
//创建一个更加基础的基类
class Base {
//把更加基础的方法和成员写到Base类
}
// A类
class A extends Base {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
/*
B类继承了A
增加了一个新功能:完成两个数相加,然后和9求和
*/
class B extends Base {
//如果B需要使用A类的方法,使用组合关系
private A a = new A();
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
//我们仍然想使用A的方法
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
- 一个软件实体中类,模块和方法,应该对扩展开放【提供方】,对修改关闭【使用方】,用抽象构建框架,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其实原则,以及使用设计模式的目的就是为了遵循开闭原则。
当新增画三角形,不仅要对提供方进行扩展,而且要对使用方的代码进行大量修改。

public class Ocp {
public static void main(String[] args) {
//使用看看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
//新增画三角形
graphicEditor.drawShape(new Triangle());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3) //新增画三角形
drawTriangle(s);
}
//绘制矩形
public void drawRectangle(Shape r) {
System.out.println(" 绘制矩形 ");
}
//绘制圆形
public void drawCircle(Shape r) {
System.out.println(" 绘制圆形 ");
}
//新增绘制三角形
public void drawTriangle(Shape r) {
System.out.println(" 绘制三角形 ");
}
}
//Shape类,基类
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
//新增画三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
改进后的方法
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,调用draw方法
public void drawShape(Shape s) {
s.draw();
}
}
//Shape类,基类
abstract class Shape {
public abstract void draw();//抽象方法
}
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println(" 绘制矩形 ");
}
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println(" 绘制圆形 ");
}
}
//新增画三角形
class Triangle extends Shape {
@Override
public void draw() {
System.out.println(" 绘制三角形 ");
}
}
//新增一个图形
class OtherGraphic extends Shape {
@Override
public void draw() {
System.out.println(" 绘制其它图形 ");
}
}
一个对象应该对其他对象保持最少的了解
类与类关系越密切,耦合度越大
迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于
被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间
是朋友关系。
耦合的方式很多,依赖,关联,组合,聚合等。其中,
我们称出现成员变量,方法参数,方法返回值 中的类为直接的朋友,
而出现在局部变量中的类是陌生朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
//客户端
public class Demeter1 {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id和学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
//3. 违反了 迪米特法则
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
改进后方法
//客户端
public class Demeter1 {
public static void main(String[] args) {
System.out.println("~~~使用迪米特法则的改进~~~");
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
- 迪米特法则的核心是降低类之间的耦合
- 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是
要求完全没有依赖关系, 因为绝对不存在耦合关系类或对象是不存在的。
原则是尽量使用合成/聚合的方式,而不是使用继承

依赖、关联、泛化(继承)、实现、聚合与组合。
依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类.

关联(Association)关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。
关联可以是双向的,也可以是单向的
在 UML 类图中,双向的关联可以用带两个箭头或者没有箭头的实线来表示,单向的关联用带一个箭头的实线来表示,箭头从使用类指向被关联的类。也可以在关联线的两端标注角色名,代表两种不同的角色。

泛化(Generalization)关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系。
在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类.

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

聚合(Aggregation)关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,是 has-a 的关系。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。

组合(Composition)关系也是关联关系的一种,也表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系,是 contains-a 关系。在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。图 7 所示是头和嘴的关系图。

| 创建时间: | 2022/7/19 23:11 |
| 更新时间: | 2022/7/19 23:12 |
| 作者: | Chris |
https://blog.csdn.net/m0_67391377/article/details/123756444
| 创建时间: | 2022/7/13 17:35 |
| 来源: | https://mp.weixin.qq.com/s/GUXX4m4bAIomWHsHJHCAog |
| 创建时间: | 2022/7/13 8:58 |
| 来源: | https://mp.weixin.qq.com/s/mNXBCXbRGiHukdBNmVh2vg |
| 创建时间: | 2020/10/14 22:57 |
| 更新时间: | 2022/7/11 10:30 |
| 作者: | Chris |
| Name | Desc | Key | Value | Synchronized |
|---|---|---|---|---|
| HashMap | 根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度 | 允许为null | 允许为null | 非同步 |
| TreeMap | 能够把它保存的记录根据键(key)排序,默认是按升序排序,也可以指定排序的比较器 | 不允许为null | 允许为null | 非同步 |
| Hashtable | 与HashMap类似 | 不允许为null | 不允许为null | 支持线程同步 |
| LinkedHashMap | 保存了记录的写入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先写入的 | 允许为null | 允许为null | 非同步 |
| WeakHashMap | 与HashMap类似 | 允许为null | 允许为null | 非同步 |
WeakHashMap 继承于AbstractMap,实现了Map接口。
和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
不过WeakHashMap的键是“弱键”。
在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
这个“弱键”的原理呢?
通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。
和HashMap一样,WeakHashMap是不同步的。
可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap
线程安全问题
WeakHashMap<String, String> weakHashMap=new WeakHashMap<String, String>();
Map<String, String> intsmaze=Collections.synchronizedMap(weakHashMapintsmaze);
实现步骤是:
| 创建时间: | 2022/7/8 10:09 |
| 更新时间: | 2022/7/8 10:11 |
| 作者: | Chris |

激活网站
http://www.cicoding.cn/other/jrebel-activation/

| 创建时间: | 2022/7/7 14:31 |
| 来源: | https://mp.weixin.qq.com/s/dcl1dc2CCQ5RYoR3bsfweQ |
Future虽然可以实现获取异步执行结果的需求,但是它没有提供通知的机制,我们无法得知Future什么时候完成。
- 要么使用阻塞,在
future.get()的地方等待future返回的结果,这时又变成同步操作。- 要么使用
isDone()轮询地判断Future是否完成,这样会耗费CPU的资源。
CompletableFuture能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中。
CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

Runnable
Consumer
Supplier
Function
runAsync 与 supplierAsync是 CompletableFutre 的静态方法;
而thenAccept、thenAsync、thenApply是 CompletableFutre 的成员方法
因为初始的时候没有 CompletableFuture 对象,也没有参数可传,所以提交的只能是 Runnable 或者 Supplier,只能是静态方法;
通过静态方法生成 CompletableFuture 对象之后,便可以链式地提交其他任务了,这个时候就可以提交 Runnable、Consumer、Function且都是成员方法
CompletableFuture实现了Future接口,所以它也具有Future的特性:调用 get()方法会阻塞在那,直到结果返回。
@SneakyThrows
@Test
public void asyn1() {
CompletableFuture<String> cf = new CompletableFuture<>();
//complete方法完成该Future,否则在调用cf.get()阻塞主线程,等待返回结果
cf.complete("hello future!");
//调用者阻塞,等待返回结果
String s = cf.get();
System.out.println(s);
}
通过静态方法生成 CompletableFuture 对象之后,便可以链式地提交其他任务了
@SneakyThrows
@Test
public void runAsync() {
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
try {
System.out.println("task is running!");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).thenRun(() -> System.out.println("call back method after getting result!"));
//主线程阻塞,等待任务执行完成
cf.get();
}
@SneakyThrows
@Test
public void supplyAsync() {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task has done!";
}).thenApplyAsync(s -> s + ", call back method after getting result!");
//主线程阻塞,等待任务执行完成
String s = cf.get();
System.out.println(s);
}
thenRun后面跟的是一个无参数、无返回值的方法,即 Runnable.
thenAccept后面跟的是一个有参数、无返回值的方法,称为 Consumer.
thenApply后面跟的是一个有参数、有返回值的方法,称为 Function.
thenApplyAsync与thenApply的区别在于
前者是将job2提交到线程池中异步执行,实际执行job2的线程可能是另外一个线程
后者是由执行job1的线程立即执行job2,即两个job都是同一个线程执行的
指定某个任务执行异常时执行的回调方法,会将抛出异常作为参数传递到回调方法中
如果该任务正常执行会exceptionally方法返回的CompletionStage的result就是该任务正常执行的结果
@SneakyThrows
@Test
public void testExceptionally() {
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread() + "job1 start, time->" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常结果需要改为false
if (true) {
System.out.println("throw exception!");
throw new RuntimeException("test exceptionally");
}
return 1.1;
});
CompletableFuture<Double> cf2 = cf.exceptionally((exception) -> {
System.out.println(Thread.currentThread() + " start, time->" + System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("error stack trace->");
exception.printStackTrace();
System.out.println(Thread.currentThread() + " exit, time->" + System.currentTimeMillis());
return 1.2;
});
System.out.println(cf2.get());
}
正常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657161758634
1.1
异常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657161554581
throw exception!
Thread[ForkJoinPool.commonPool-worker-1,5,main] start, time->1657161559581
error stack trace->
java.util.concurrent.CompletionException: java.lang.RuntimeException: test exceptionally
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1592)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException: test exceptionally
Thread[ForkJoinPool.commonPool-worker-1,5,main] exit, time->1657161561583
at com.thread.future.async.CompletableFutureSourceCode.lambda$testExceptionally$7(CompletableFutureSourceCode.java:139)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1590)
... 7 more
1.2
当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法,
如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致,如果该任务正常执行,
则get方法返回执行结果,如果是执行异常,则get方法抛出异常
@SneakyThrows
@Test
public void testWhenComplete() {
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread() + "job1 start, time->" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常结果需要改为false
if (true) {
System.out.println("throw exception!");
throw new RuntimeException("test exceptionally");
}
return 1.1;
});
CompletableFuture<Double> cf2 = cf.whenComplete((result, exception) -> {
System.out.println(Thread.currentThread() + " start, time->" + System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("error stack trace->");
if (Objects.nonNull(exception)) {
exception.printStackTrace();
}
System.out.println(Thread.currentThread() + " exit, time->" + System.currentTimeMillis());
System.out.println("result:" + result);
});
System.out.println(cf2.get());
}
正常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657162560549
Thread[ForkJoinPool.commonPool-worker-1,5,main] start, time->1657162565550
error stack trace->
Thread[ForkJoinPool.commonPool-worker-1,5,main] exit, time->1657162567550
result:1.1
1.1
异常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657162369911
throw exception!
Thread[ForkJoinPool.commonPool-worker-1,5,main] start, time->1657162374911
error stack trace->
java.util.concurrent.CompletionException: java.lang.RuntimeException: test exceptionally
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1592)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException: test exceptionally
at com.thread.future.async.CompletableFutureSourceCode.lambda$testWhenComplete$9(CompletableFutureSourceCode.java:176)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1590)
... 7 more
Thread[ForkJoinPool.commonPool-worker-1,5,main] exit, time->1657162376913
result:null
跟whenComplete基本一致,区别在于handle的回调方法有返回值,
且handle方法返回的CompletableFuture的result是回调方法的执行结果或者回调方法执行期间抛出的异常,与原始CompletableFuture的result无关了。
@SneakyThrows
@Test
public void testHandle() {
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread() + "job1 start, time->" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (true) {
System.out.println("throw exception!");
throw new RuntimeException("test exceptionally");
}
return 1.1;
});
CompletableFuture<String> handle = cf.handle((result, exception) -> {
System.out.println(Thread.currentThread() + " start, time->" + System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("error stack trace->");
if (Objects.nonNull(exception)) {
exception.printStackTrace();
}
System.out.println(Thread.currentThread() + " exit, time->" + System.currentTimeMillis());
if (Objects.isNull(exception)) {
return "run success";
} else {
return "run error";
}
});
System.out.println(handle.get());
}
正常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657163319080
Thread[ForkJoinPool.commonPool-worker-1,5,main] start, time->1657163324081
error stack trace->
Thread[ForkJoinPool.commonPool-worker-1,5,main] exit, time->1657163326081
run success
异常结果
Thread[ForkJoinPool.commonPool-worker-1,5,main]job1 start, time->1657163045499
throw exception!
Thread[ForkJoinPool.commonPool-worker-1,5,main] start, time->1657163050500
error stack trace->
java.util.concurrent.CompletionException: java.lang.RuntimeException: test exceptionally
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1592)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
Thread[ForkJoinPool.commonPool-worker-1,5,main] exit, time->1657163052502
run success
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException: test exceptionally
at com.thread.future.async.CompletableFutureSourceCode.lambda$testHandle$11(CompletableFutureSourceCode.java:215)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1590)
... 7 more
同步获取结果
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent) //结果已经计算完则返回结果或者抛出异常,否则返回给定的valueIfAbsent值。
public T join()
join()与get()区别在于join()返回计算的结果或者抛出一个unchecked异常(CompletionException),而get()返回一个具体的异常.
future.get()在等待执行结果时,程序会一直block,如果此时调用complete(T t)会立即执行。
complete(T t) 完成异步操作,并返回future的结果
completeExceptionally(Throwable ex) 异步执行不正常的结束
future调用complete(T t)会立即执行。但是complete(T t)只能调用一次,后续的重复调用会失效。
如果future已经执行完毕能够返回结果,此时再调用complete(T t)则会无效。
@Slf4j
public class CompleableFutrueTest {
@Test
public void allOfTest() {
List<CompletableFuture<String>> cfList = CollUtil.newArrayList();
for (int i = 0; i < 10; i++) {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
String result = null;
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " begin sleep");
TimeUnit.SECONDS.sleep(3);
System.out.println(threadName + " end sleep");
result = doSomething(threadName);
} catch (Exception e) {
e.printStackTrace();
}
return result;
});
cfList.add(cf);
}
CompletableFuture<Void> allCF = CompletableFuture.allOf(cfList.toArray(new CompletableFuture[0]));
try {
allCF.get();
CompletableFuture<List<String>> resultCF = allCF.thenApply(v -> cfList.stream().map(cf -> {
try {
return cf.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}).collect(Collectors.toList()));
List<String> result = resultCF.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public String doSomething(String jobNumber) {
if (StrUtil.equals(jobNumber, "ForkJoinPool.commonPool-worker-2")) {
throw new RuntimeException("ForkJoinPool.commonPool-worker-2 is exception");
} else {
for (int i = 0; i < 10; i++) {
System.out.println(jobNumber + " is outputing:" + i);
}
}
return jobNumber + " is done!";
}
}
| 创建时间: | 2022/7/1 11:48 |
| 来源: | https://mp.weixin.qq.com/s/8RTgljhbRgy5A-HbOBe8Ng |
| 创建时间: | 2022/7/1 8:05 |
| 来源: | https://mp.weixin.qq.com/s/OJBqW0uNKVQ0hUYTly_5Eg |
| 创建时间: | 2022/6/30 16:47 |
| 更新时间: | 2022/6/30 17:03 |
| 作者: | Chris |
Spring提供的计时器StopWatch对于秒、毫秒为单位方便计时的程序,
尤其是单线程、顺序执行程序的时间特性的统计输出支持比较好。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
@Test
public void test1() throws InterruptedException {
StopWatch stopWatch = new StopWatch();
// 任务一模拟休眠3秒钟
stopWatch.start("TaskOneName");
Thread.sleep(1000 * 3);
System.out.println("当前任务名称:" + stopWatch.currentTaskName());
stopWatch.stop();
// 任务一模拟休眠10秒钟
stopWatch.start("TaskTwoName");
Thread.sleep(1000 * 10);
System.out.println("当前任务名称:" + stopWatch.currentTaskName());
stopWatch.stop();
// 任务一模拟休眠10秒钟
stopWatch.start("TaskThreeName");
Thread.sleep(1000 * 10);
System.out.println("当前任务名称:" + stopWatch.currentTaskName());
stopWatch.stop();
// 打印出耗时
System.out.println(stopWatch.prettyPrint());
System.out.println(stopWatch.shortSummary());
// stop后它的值为null
System.out.println(stopWatch.currentTaskName());
// 最后一个任务的相关信息
System.out.println(stopWatch.getLastTaskName());
System.out.println(stopWatch.getLastTaskInfo());
// 任务总的耗时 如果你想获取到每个任务详情(包括它的任务名、耗时等等)可使用
System.out.println("所有任务总耗时:" + stopWatch.getTotalTimeMillis());
System.out.println("任务总数:" + stopWatch.getTaskCount());
System.out.println("所有任务详情:" + Arrays.toString(stopWatch.getTaskInfo()));
}
结果
当前任务名称:TaskOneName
当前任务名称:TaskTwoName
当前任务名称:TaskThreeName
StopWatch '': running time = 23002371100 ns
---------------------------------------------
ns % Task name
---------------------------------------------
3000873900 013% TaskOneName
10000460000 043% TaskTwoName
10001037200 043% TaskThreeName
StopWatch '': running time = 23002371100 ns
null
TaskThreeName
org.springframework.util.StopWatch$TaskInfo@5e5d171f
所有任务总耗时:23002
任务总数:3
所有任务详情:[org.springframework.util.StopWatch$TaskInfo@24313fcc, org.springframework.util.StopWatch$TaskInfo@7d20d0b, org.springframework.util.StopWatch$TaskInfo@5e5d171f]

注意事项
1. StopWatch对象不是设计为线程安全的,并且不使用同步。
2. 一个StopWatch实例一次只能开启一个task,不能同时start多个task
3. 在该task还没stop之前不能start一个新的task,必须在该task stop之后才能开启新的task
4. 若要一次开启多个,需要new不同的StopWatch实例
StopWath是 apache commons lang3 包下的一个任务执行时间监视器,与我们平时常用的秒表的行为比较类似

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>;
</dependency>
@Test
public void testApache() throws InterruptedException {
//创建后立即start,常用
StopWatch watch = StopWatch.createStarted();
// StopWatch watch = new StopWatch();
// watch.start();
Thread.sleep(1000);
System.out.println(watch.getTime());
System.out.println("统计从开始到现在运行时间:" + watch.getTime() + "ms");
Thread.sleep(1000);
watch.split();
System.out.println("从start到此刻为止的时间:" + watch.getTime());
System.out.println("从开始到第一个切入点运行时间:" + watch.getSplitTime());
Thread.sleep(1000);
watch.split();
System.out.println("从开始到第二个切入点运行时间:" + watch.getSplitTime());
// 复位后, 重新计时
watch.reset();
watch.start();
Thread.sleep(1000);
System.out.println("重新开始后到当前运行时间是:" + watch.getTime());
// 暂停 与 恢复
watch.suspend();
System.out.println("暂停2秒钟");
Thread.sleep(2000);
// 上面suspend,这里要想重新统计,需要恢复一下
watch.resume();
System.out.println("恢复后执行的时间是:" + watch.getTime());
Thread.sleep(1000);
watch.stop();
System.out.println("花费的时间》》" + watch.getTime() + "ms");
// 直接转成s
System.out.println("花费的时间》》" + watch.getTime(TimeUnit.SECONDS) + "s");
}
结果
1002
统计从开始到现在运行时间:1006ms
从start到此刻为止的时间:2017
从开始到第一个切入点运行时间:2017
从开始到第二个切入点运行时间:3017
重新开始后到当前运行时间是:1000
暂停2秒钟
恢复后执行的时间是:1000
花费的时间》》2001ms
花费的时间》》2s
| 创建时间: | 2022/6/30 14:57 |
| 更新时间: | 2022/6/30 15:23 |
| 来源: | https://mp.weixin.qq.com/s/0oD0HTFBr3UxL2AxXMqknQ |
这里重点讲下基于spring、Apache的使用
* 本实例的唯一 Id,用于在日志或控制台输出时区分的。 */ * 是否保持一个 taskList 链表 * 每次停止计时时,会将当前任务放入这个链表,用以记录任务链路和计时分析 */ * 任务链表 * 用来存储每个task的信息, taskInfo由taskName 和 totoalTime组成 */ * 当前任务的开始时间 */ * */ * 当前任务名称 */ * 最后一个任务的信息 */ * 任务总数 */ * 程序执行时间 */

| 创建时间: | 2022/6/28 16:20 |
| 更新时间: | 2022/6/28 16:24 |
| 作者: | Chris |
使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
- 1、内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
- 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
- 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 5、内部类提供了更好的封装,除了该外部类,其他类都不能访问。
内部类是个编译时的概念,一旦编译成功后,它就与外部类属于两个完全不同的类(当然他们之间还是有联系的)。
对于一个名为 OuterClass 的外部类和一个名为 InnerClass 的内部类,在编译成功后,会出现这样两个class文件:OuterClass.class
OuterClass$InnerClass.class。
在成员内部类中要注意两点,
第一:成员内部类中不能存在任何static的变量和方法;第二:成员内部类是依附于外部类的,所以只有先创建了外部类才能够创建内部类。
- 在外部类内部创建内部类对象
Inner inner = new Inner();
- 在外部类外部创建内部类对象,
Outter.Inner inner = new Outter().new Inner();第三: 在内部类内部使用隐藏的外部类对象
隐藏的 this
@Getter
public class Outer {
private String name = "outer";
private String outer = "I'm outer";
private Inner innerInstance;
// 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时
public Inner getInnerInstance() {
if (null == innerInstance) {
innerInstance = new Inner();
}
return innerInstance;
}
@Getter
class Inner {
private String name = "inner";
public Inner() {
System.out.println("construct inner cls");
System.out.println("outer:" + outer + ", " + name);
//if you wanna visit outer'name
System.out.println("outer:" + outer + ", " + Outer.this.name);
}
}


在外部类的外部创建内部类
public class TestInner {
public static void main(String[] args) {
Outer outer = new Outer();
// the first way to create inner instance
// Outer.Inner inner = outer.new Inner();
// the second way to create inner instance
Outer.Inner inner = outer.getInnerInstance();
System.out.println(outer.getName());
System.out.println(inner.getName());
}
}
定义在外部类的内部,使用static修饰,类比静态方法,静态内部类不需要产生外部类对象就能使用
外部类内部:与成员内部类一样
Inner inner = new Inner();在外部类外部创建内部类对象
Outer2.Inner inner = new Outer2.Inner();静态内部类不能访问外部类的成员域,但能访问静态域。
@Getter
public class Outer2 {
private String name = "outer";
private static String outer="I'm outer";
@Getter
static class Inner {
private String name = "inner";
public static void descInner() {
System.out.println("outer:" + outer);
}
}
}
public class TestStaticInner {
public static void main(String[] args) {
Outer2.Inner inner = new Outer2.Inner();
System.out.println(inner.getName());
Outer2.Inner.descInner();
}
}
定义在方法内部:类比局部变量
- 对外部完全隐藏,因此方法内部类不能有任何访问修饰符
- 方法内部类没有访问形参是,这个形参是可以在方法中随意修改的,一旦方法内部类中使用了形参,这个形参必须被声明为final。
必须继承一个抽象类或者实现一个接口
没有构造方法
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@Test
public void test1() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hellow runnable r1.");
}
};
r1.run();
//Lambda表达式做为左边接口的一个实例
Runnable r2 = () -> System.out.println("hellow runnable r2.");
r2.run();
}


| 创建时间: | 2021/3/24 11:19 |
| 更新时间: | 2022/6/9 19:05 |
| 作者: | Chris |
- AutoCloseable接口,表示一种不再使用时需要关闭的资源。这个接口下只有一个方法,close()。这个方法在try-with-resource语法下会被自动调用,支持抛出Exception,当然它也鼓励抛出更详细的异常。
- close()建议不要抛出线程中断的 InterruptedException。
- 对这个接口的实现,规范强烈建议close()是幂等的,也就是说多次调用close()方法和一次调用的结果是一样的。
@Test
public void TestTryWithResource() {
try (MyResource1 resource1 = new MyResource1();
MyResource2 resource2 = new MyResource2()) {
resource1.readResource();
resource2.readResource();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MyResource1 implements Closeable {
@Override
public void close() throws IOException {
System.out.println("close resource1");
}
public void readResource() {
System.out.println("read resource1");
}
}
public class MyResource2 implements Closeable {
@Override
public void close() throws IOException {
System.out.println("close resource2");
}
public void readResource() {
System.out.println("read resource2");
}
}
| 创建时间: | 2020/9/2 15:08 |
| 更新时间: | 2022/5/2 12:27 |
| 作者: | Chris |
http://c.biancheng.net/view/1343.html
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解
某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
定义一个用于创建产品的接口,由子类决定生产什么产品。
提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现, 从而降低了抽象和实现这两个可变维度的耦合度。
动态的给对象增加一些职责,即增加其额外的功能。
为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
运用共享技术来有效地支持大量细粒度对象的复用。
将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
允许一个对象在其内部状态发生改变时改变其行为能力。
多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类.

关联(Association)关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。
关联可以是双向的,也可以是单向的
在 UML 类图中,双向的关联可以用带两个箭头或者没有箭头的实线来表示,单向的关联用带一个箭头的实线来表示,箭头从使用类指向被关联的类。也可以在关联线的两端标注角色名,代表两种不同的角色。

聚合(Aggregation)关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,是 has-a 的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。

组合(Composition)关系也是关联关系的一种,也表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系,是 cxmtains-a 关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。图 7 所示是头和嘴的关系图。

泛化(Generalization)关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系。
在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类.

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

定义
软件实体对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification)
开闭原则的含义是:
当应用的需求改变时,在不修改软件实体的源代码前提下,可以扩展模块的功能,使其满足新的需求。
软件实体包括
- 项目中划分出的模块
- 类与接口
- 方法
- 里氏替换原则是实现开闭原则的重要方式之一
- 里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
- 如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
- 如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类
package principle;
public class LSPtest
{
public static void main(String[] args)
{
Bird bird1=new Swallow();
Bird bird2=new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飞行300公里:");
try
{
System.out.println("燕子将飞行"+bird1.getFlyTime(300)+"小时.");
System.out.println("几维鸟将飞行"+bird2.getFlyTime(300)+"小时。");
}
catch(Exception err)
{
System.out.println("发生错误了!");
}
}
}
//鸟类
class Bird
{
double flySpeed;
public void setSpeed(double speed)
{
flySpeed=speed;
}
public double getFlyTime(double distance)
{
return(distance/flySpeed);
}
}
//燕子类
class Swallow extends Bird{}
//几维鸟类
class BrownKiwi extends Bird
{
public void setSpeed(double speed)
{
flySpeed=0;
}
}
定义:
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
其核心思想是:要面向接口编程,不要面向实现编程。
目的:
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。
实现原则:
1.每个类尽量提供接口或抽象类,或者两者都具备。
2.变量的声明类型尽量是接口或者是抽象类。
3.任何类都不应该从具体类派生。
4.使用继承时尽量遵循里氏替换原则
package principle;
public class DIPtest
{
public static void main(String[] args)
{
Customer wang=new Customer();
System.out.println("顾客购买以下商品:");
wang.shopping(new ShaoguanShop());
wang.shopping(new WuyuanShop());
}
}
//商店
interface Shop
{
public String sell(); //卖
}
//韶关网店
class ShaoguanShop implements Shop
{
public String sell()
{
return "韶关土特产:香菇、木耳……";
}
}
//婺源网店
class WuyuanShop implements Shop
{
public String sell()
{
return "婺源土特产:绿茶、酒糟鱼……";
}
}
//顾客
class Customer
{
public void shopping(Shop shop)
{
//购物
System.out.println(shop.sell());
}
}
定义是:
只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
含义是:
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。
目的是:
降低类之间的耦合度,提高模块的相对独立性。
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
实现原则
- 在类的划分上,应该创建低耦合的类。类与类之间的耦合越低,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能
定义:指一个类只有一个实例,且该类能自行创建这个类的实例。
单例模式有3个特点:
1.单例类只有一个实例对象;
2.该单例对象必须由单例类自行创建;
3.单例类对外提供一个访问该单例的全局访问点;
定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象
原型模式有2个特点:
1.对象之间相同或相似,即只是个别的几个属性不同的时候。
2.对象的创建过程比较麻烦,但复制比较简单的时候。
定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
模式的结构
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

其缺点是:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,
该模式的主要优点如下:
- 各个具体的建造者相互独立,有利于系统的扩展。
- 客户端不必知道产品内部组成的细节,便于控制细节风险。
其缺点如下:
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,该模式会增加很多的建造者类。
模式的应用场景
建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。
- 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
- 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。
定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式
装饰模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态
备忘录模式的主要角色如下
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
| 创建时间: | 2022/4/30 14:00 |
| 更新时间: | 2022/4/30 14:12 |
| 作者: | Chris |
| 来源: | https://www.jb51.net/article/214989.htm |
File -> Settings -> Plugins 搜索 PlantUML ,找到 PlantUML integration 并安装。

IDEA 安装 PlantUML 插件之后发现光有插件还不能渲染类图,还需要 Graphviz 的支持。
https://graphviz.org/download/

打开电脑系统属性选择高级 -> 环境变量添加path变量,变量值为之前安装路径下的bin目录。

配置完成之后打开 cmd 输入:dot -version
如果版本号打印成功,说明环境配置完成

成功之后重新启动 IDEA 即可创建 PlantUML File 了

点击之后可以发现可以创建很多 UML 图,
例如:时序图、用例图、类图、活动图、组件图、状态图、对象图。
各种图的具体说明https://plantuml.com/

| 创建时间: | 2022/4/29 10:44 |
| 来源: | https://mp.weixin.qq.com/s/sxC8rQqFdMV_DiQk-YYUOg |
| 创建时间: | 2022/4/26 8:46 |
| 来源: | https://mp.weixin.qq.com/s/yS4guSS1JnLE83u_vqDIvA |

user.getAddress().getProvince();if(user!=null){ Address address = user.getAddress(); if(address!=null){ String province = address.getProvince(); }}Optional(T value),即构造函数,它是private权限的,不能由外部调用的。其余三个函数是public权限,供我们所调用。那么,Optional的本质,就是内部储存了一个真实的值,在构造的时候,就直接判断其值是否为空。好吧,这么说还是比较抽象。直接上Optional(T value)构造函数的源码,如下图所示
public static <T> Optional<T> of(T value) { return new Optional<>(value);}of(T value)函数所构造出的Optional对象,当Value值为空时,依然会报NullPointerException。of(T value)函数所构造出的Optional对象,当Value值不为空时,能正常构造Optional对象。public final class Optional<T> { //省略.... private static final Optional<?> EMPTY = new Optional<>(); private Optional() { this.value = null; } //省略... public static<T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; }}empty()的作用就是返回EMPTY对象。ofNullable(T value)的作用了,上源码 public static <T> Optional<T> ofNullable(T value) { return value == null ? empty() : of(value);}of(T value)的区别就是,当value值为null时,of(T value)会报NullPointerException异常;ofNullable(T value)不会throw Exception,ofNullable(T value)直接返回一个EMPTY对象。ofNullable函数而不用of函数呢?NullPointerException。而是要立即报告,这种情况下就用Of函数。但是不得不承认,这样的场景真的很少。博主也仅在写junit测试用例中用到过此函数。orElse和orElseGet的用法如下所示,相当于value值为null时,给予一个默认值:@Testpublic void test() { User user = null; user = Optional.ofNullable(user).orElse(createUser()); user = Optional.ofNullable(user).orElseGet(() -> createUser());}public User createUser(){ User user = new User(); user.setName("zhangsan"); return user;}orElse函数依然会执行createUser()方法,而orElseGet函数并不会执行createUser()方法,大家可自行测试。User user = null;Optional.ofNullable(user).orElseThrow(()->new Exception("用户不存在"));3、map(Function mapper)和flatMap(Function> mapper) public final class Optional<T> { //省略.... public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } } //省略... public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Objects.requireNonNull(mapper.apply(value)); } }}Function<? super T, ? extends U>,而flapMap的入参类型为Function<? super T, Optional<U>>。public class User { private String name; public String getName() { return name; }}String city = Optional.ofNullable(user).map(u-> u.getName()).get();public class User { private String name; public Optional<String> getName() { return Optional.ofNullable(name); }}String city = Optional.ofNullable(user).flatMap(u-> u.getName()).get();isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作。这两个函数的源码如下public final class Optional<T> { //省略.... public boolean isPresent() { return value != null; } //省略... public void ifPresent(Consumer<? super T> consumer) { if (value != null) consumer.accept(value); }}if (user != null){ // TODO: do something}User user = Optional.ofNullable(user);if (Optional.isPresent()){ // TODO: do something}ifPresent(Consumer<? super T> consumer),用法也很简单,如下所示Optional.ofNullable(user).ifPresent(u->{ // TODO: do something});public final class Optional<T> { //省略.... Objects.requireNonNull(predicate); if (!isPresent()) return this; else return predicate.test(value) ? this : empty();}Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty。Optional<User> user1 = Optional.ofNullable(user).filter(u -> u.getName().length()<6);public String getCity(User user) throws Exception{ if(user!=null){ if(user.getAddress()!=null){ Address address = user.getAddress(); if(address.getCity()!=null){ return address.getCity(); } } } throw new Excpetion("取值错误"); }public String getCity(User user) throws Exception{ return Optional.ofNullable(user) .map(u-> u.getAddress()) .map(a->a.getCity()) .orElseThrow(()->new Exception("取指错误"));}if(user!=null){ dosomething(user);} Optional.ofNullable(user) .ifPresent(u->{ dosomething(u);});public User getUser(User user) throws Exception{ if(user!=null){ String name = user.getName(); if("zhangsan".equals(name)){ return user; } }else{ user = new User(); user.setName("zhangsan"); return user; }}public User getUser(User user) { return Optional.ofNullable(user) .filter(u->"zhangsan".equals(u.getName())) .orElseGet(()-> { User user1 = new User(); user1.setName("zhangsan"); return user1; });}转自:zjhred链接:blog.csdn.net/zjhred/article/details/84976734
| 创建时间: | 2022/3/20 14:31 |
| 更新时间: | 2022/4/18 15:36 |
| 作者: | Chris |
| 来源: | https://www.bilibili.com/video/BV1cf4y157sz?p=5 |
https://bright-boy.gitee.io/technical-notes
MQ全称message queue(消息队列),是在消息传输的过程中保存消息的容器。
多用于分布式系统间进行通信
queue(队列):一种数据结构,特征为‘先进先出’
- 应用解耦
- 异步提速
- 削峰填谷

当生产者生产完消息后,无需等待就可以进行下一步业务逻辑。
需要根据消息完成特定功能的业务组件只需要简单订阅消息即可完成对消息的消费。
即使后面有新增的业务组件,也只需要简单订阅消息就可以完成平滑的接入。
对超量请求可以暂存其中,以便系统后续可以慢慢处理,从而避免请求丢失或系统被压垮
例如:使用MQ之后,限制消息消费的速度为1000条/s, 这样一来,高峰期产生的数据势必会积压在MQ中,这样高峰就被削掉,但因为消息的积压,高峰期过后一段时间内,消费消息的速度还在维持在1000/s直到消费后积压的消息为至,这就叫填谷。
引入的外部依赖越多,系统的稳定性就会越差,引入的MQ 宕机就会对业务造成影响
需要解决保证MQ的高可用问题
MQ的加入增加了系统的复杂性,之前系统间是同步的远程调用,现在是通过MQ进行异步调用。
如何保证消息没有被重复消费
怎么处理消息丢失
如何保证消息传递的顺序性
A系统处理完业务通过MQ给 B,C,D三个系统发消息数据,如果B,C处理成功,但是D系统处理失败。
如何保证消息数据处理的一致性?
ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已经很低。现在的项目中已经很少使用了。
RabbitMQ是使用ErLang语言开发的一款MQ产品。其吞吐量较Kafka与RocketMQ要低,且由于其不是Java语言开发,所以公司内部对其实现定制化开发难度较大。
Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐率,常用于大数据领域的实时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Netfix,其仅支持RabbitMQ与Kafka。
RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双 11 的考验,性能与稳定性非常高。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Alibaba,其支持RabbitMQ、Kafka,但提倡使用RocketMQ。
Ali技术团队在Kafka开源后对其技术进行了深入的研究开发出一款新的MQ叫MetaMQ, 后来对MetaMQ进一步开发成为后面的RocketMQ。

消息生产者,负责生产消息。
Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。RocketMQ中的消息生产者都是以
生产者组(Producer Group)的形式出现的。
生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
例如,业务系统产生的日志写入到MQ的过程,就是消息生产的过程
再如,电商平台中用户提交的秒杀请求写入到MQ的过程,就是消息生产的过程
消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理。
RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。
消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。不过,一个Topic类型的消息可以被多个消费者组同时消费消费者组使得在消息消费方面,实现
负载均衡(将一个Topic中的不同的Queue平均分配给同一个Consumer Group的不同的Consumer,注意,并不是将消息负载均衡)和容错(一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue)的目标变得非常容易。
例如,QoS系统从MQ中读取日志,并对日志进行解析处理的过程就是消息消费的过程。
再如,电商平台的业务系统从MQ中读取到秒杀请求,并对请求进行处理的过程就是消息消费的过程。
消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。
如果超出Queue数量,则多出的Consumer将不能消费消息。

注意,
- 1 消费者组只能消费一个Topic的消息,不能同时消费多个Topic消息
- 2 一个消费者组中的消费者必须订阅完全相同的Topic
NameServer是Broker与Topic路由的注册中心,支持Broker的动态注册与发现
RocketMQ的思想来自于Kafka,而Kafka是依赖zookeeper的,所以在RocketMQ早期版本中也依赖zookeeper。从MetaQ v3.0开始,RocketMQ去掉了zookeeper的依赖,使用b了自己的NameServer
主要功能有两个:
Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测机制,检查Broker是否还存活。路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Conumser通过NameServer可以获取整个Broker集群的路由信息,从而进行消息的投递和消费。
NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,
各节点间相互不进行信息通讯。那各节点中的数据是如何进行数据同步的呢?
在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护着一个Broker列表,用来动态存储Broker的信息。
注意,这是与其它像zk、Eureka、Nacos等注册中心不同的地方。
这种NameServer的无状态方式,有什么优缺点:
优点:NameServer集群搭建简单,扩容简单。
缺点:对于Broker,必须明确指出所有NameServer地址。否则未指出的将不会去注册。也正因为如此,NameServer并不能随便扩容。因为,若Broker不重新配置,新增的NameServer对于Broker来说是不可见的,其不会向这个NameServer进行注册。
Broker节点为了证明自己是活着的,为了维护与NameServer间的长连接,会将最新的信息以心跳包的方式上报给NameServer,每 30 秒发送一次心跳。心跳包中包含 BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。
由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
NameServer中有一个定时任务,每隔 10 秒就会扫描一次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过 120 秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。
扩展:
对于RocketMQ日常运维工作,例如Broker升级,需要停掉Broker的工作。
OP需要怎么做?
OP需要将Broker的读写权限禁掉。一旦client(Consumer或Producer)向broker发送请求,都会收到broker的NO_PERMISSION响应,然后client会进行对其它Broker的重试。
当OP观察到这个Broker没有流量后,再关闭它,实现Broker从NameServer的移除。
OP:运维工程师
SRE:Site Reliability Engineer,现场可靠性工程师
RocketMQ的路由发现采用的是
Pull模型。
在RocketMQ虽然有对于消费者有DefaultMQPullConsumer和DefaultMQPushConsumer两个Api可供选择,但是底层其实都使用PullAPIWrapper这个类进行消息拉取,也就是说,RocketMQ使用的消费传递模型是Pull模型。
当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。
默认客户端每 30 秒会拉取一次最新的路由。
Push模型:推送模型。其实时性较好,是一个发布-订阅模型,需要维护一个长连接。而长连接的维护是需要资源成本的。该模型适合于的场景:
实时性要求较高
Client数量不多,Server数据变化较频繁
Pull模型:拉取模型。存在的问题是,实时性较差。
Long Polling模型:长轮询模型。其是对Push与Pull模型的整合,充分利用了这两种模型的优势,屏蔽了它们的劣势。
Broker充当着消息中转角色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。
Kafka 0.8版本之后,offset是存放在Broker中的,之前版本是存放在Zookeeper中的。
为了增强Broker性能与吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同Topic的不同Queue。不过,这里有个问题,如果某Broker节点宕机,如何保证数据不丢失呢?其解决方案是,将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主从集群,即集群中具有Master与Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。
当Master挂掉了,Slave则会自动切换为Master去工作。所以这个Broker集群是主备集群。一个Master可以包含多个Slave,但一个Slave只能隶属于一个Master。
Master与Slave 的对应关系是通过指定相同的BrokerName、不同的BrokerId 来确定的。BrokerId为 0 表示Master,非 0 表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。

从物理上来讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。读/写队列是逻辑上进行区分的概念。一般情况下,读/写队列数量是相同的。
例如,创建Topic时设置的写队列数量为 8 ,读队列数量为 4 ,此时系统会创建 8 个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到这 8 个队列,但Consumer只会消费0 1 2 3这 4 个队列中的消息,4 5 6 7 中的消息是不会被消费到的。
再如,创建Topic时设置的写队列数量为 4 ,读队列数量为 8 ,此时系统会创建 8 个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到0 1 2 3 这 4 个队列,但Consumer只会消费0 1 2 3 4 5 6 7这 8 个队列中的消息,但是4 5 6 7中是没有消息的。此时假设Consumer Group中包含两个Consumer,Consumer1消费0 1 2 3,而Consumer2消费4 5 6 7。但实际情况是,Consumer2是没有消息可消费的。
也就是说,当读/写队列数量设置不同时,总是有问题的。那么,为什么要这样设计呢?其这样设计的目的是为了,
方便Topic的Queue的缩容。

例如,原来创建的Topic中包含 16 个Queue,如何能够使其Queue缩容为 8 个,还不会丢失消息?可以动态修改写队列数量为 8 ,读队列数量不变。此时新的消息只能写入到前 8 个队列,而消费都消费的却是16 个队列中的数据。当发现后 8 个Queue中的消息消费完毕后,就可以再将读队列数量动态设置为 8 。整个缩容过程,没有丢失任何消息。
perm用于设置对当前创建Topic的操作权限:
2 表示只写, 4 表示只读, 6 表示读写。
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,
每条消息必须属于一个主题。
topic表示一类消息的集合,每个主题包含若干条消息,
每条消息只能属于一个主题,是
RocketMQ进行消息订阅的基本单位。
一个生产者可以同时发送多种Topic的消息;
而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。
对应关系
topic:message 1:n message:topic 1:1
producer:topic 1:n consumer:topic 1:1

手动创建Topic时,有两种模式:
- 集群模式:该模式下创建的Topic在该集群中,所有Broker中的Queue数量是相同的。
- Broker模式:该模式下创建的Topic在该集群中,每个Broker中的Queue数量可以不同。
自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建 4 个Queue。
为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
Topic是消息的一级分类,Tag是消息的二级分类。
Topic:货物
tag=上海
tag=江苏
tag=浙江
------- 消费者 -----
topic=货物 tag = 上海
topic=货物 tag = 上海|浙江
topic=货物 tag = *
存储消息的物理实体。
一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。
一个Topic的Queue也被称为一个Topic中消息的分区(Partition)。
一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费。
一个Queue中的消息不允许被同一个消费者组中的多个消费者同时消费。


在学习参考其它相关资料时,还会看到一个概念:分片(Sharding)。分片不同于分区。在RocketMQ中,分片指的是存放相应Topic的Broker。每个分片中会创建出相应数量的分区,即Queue,每个Queue的大小都是相同的。

RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有
业务标识的Key,以方便对消息的查询。
不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId)
当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。
msgId、offsetMsgId与key都称为消息标识。
msgId:
- 由producer端生成,其生成规则为:producerIp + 进程pid + MessageClientIDSetter类的ClassLoader的hashCode +当前时间 + AutomicInteger自增计数器
offsetMsgId:
- 由broker端生成,其生成规则为:brokerIp + 物理分区的offset(Queue中的偏移量)
key:由用户指定的业务相关的唯一标识

NamerServer 存放broker的IP地址信息
broker 部署着rocketmq的机器就是一个broker
生产者,消息者,broker启动之后都会把各自的IP信息注册到NamerServer里面,并与NameServer建立长连接,
Producer启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息,即当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每 30 秒从NameServer更新一次路由信息。
Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每 30 秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
Broker启动后会与配置中的每一个NameServer保持长连接,并每30秒向NameServer发送心跳包。
NamerServer除了记录IP信息外还负责通过心跳监听与各组件之间的连通性
当生产者生产消息时,先通过NamerServer找到可用的borker,然后把消息存入可用boker里面对应topic里面
消息者通过拉取模式或监听模式来获取boker里面对应topic里面的消息
消息者如何知道MQ里面有消息

复制策略是Broker的Master与Slave间的数据同步方式。
分为同步复制与异步复制:
同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK
异步复制:消息写入master后,master立即向producer返回成功ACK,无需等待slave同步数据成功
异步复制策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量
刷盘策略指的是broker中消息的
落盘方式,即消息发送到broker内存后消息持久化到磁盘的方式。分为同步刷盘与异步刷盘.
同步刷盘:当消息持久化到broker的磁盘后才算是消息写入成功。异步刷盘:当消息写入到broker的内存后即表示消息写入成功,无需等待消息持久化到磁盘。
1 . 异步刷盘策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量
2 . 消息写入到Broker的内存,一般是写入到了PageCache
3 . 对于异步 刷盘策略,消息会写入到PageCache后立即返回成功ACK。但并不会立即做落盘操作,而是当PageCache到达一定量时会自动进行落盘。
只有一个broker(其本质上就不能称为集群)。
这种方式也只能是在测试时使用,生产环境下不能使用,因为存在单点问题。
broker集群仅由多个master构成,不存在Slave。同一Topic的各个Queue会平均分布在各个master节点上。
- 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
- 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅(不可消费),消息实时性会受到影响。
以上优点的前提是,这些Master都配置了RAID10磁盘阵列。如果没有配置,一旦出现某Master宕机,则会发生大量消息丢失的情况。
broker集群由多个master构成,每个master又配置了多个slave(在配置了RAID磁盘阵列的情况下,一个master一般配置一个slave即可)。
master与slave的关系是主备关系,即master负责处理消息的读写请求,而slave仅负责消息的备份与master宕机后的角色切换。
异步复制即前面所讲的
复制策略中的异步复制策略,即消息写入master成功后,master立即向producer返回成功ACK,无需等待slave同步数据成功。
该模式的最大特点之一是,当master宕机后slave能够
自动切换为master。不过由于slave从master的同步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能会存在少量消息的丢失问题。
Slave从Master同步的延迟越短,其可能丢失的消息就越少
对于Master的RAID磁盘阵列,若使用的也是异步复制策略,同样也存在延迟问题,同样也可能会丢失消息。但RAID阵列的秘诀是
(微秒级)的(因为是由硬盘支持的),所以其丢失的数据量会更少。
该模式是
多Master多Slave模式的同步复制实现。所谓同步双写,指的是消息写入master成功后,master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会返回成功ACK,也即双写。该模式与
异步复制模式相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。该模式存在一个大的问题:对于目前的
版本4.9.0,Master宕机后,Slave不会自动切换到Master。
部署配置:同步双写-异步刷盘
创建生产者
package com.chris.rocketmq;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
/**
* @author Chris
* @date 2022-03-24 6:35 PM
*/
public class Producer {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException,
InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-producer-group-001");
producer.setNamesrvAddr("master:9876");
producer.start();
System.out.println("producer has started!");
for (int i = 0; i < 10; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-001" /* Topic */, "TagA" /* Tag */,
("Hello Chris " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
//Call send message to deliver message to one of brokers.
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}
创建消息者, 在消费组
rocketmq-consumer-group-001启动两个消费者实例
consumer.registerMessageListener
new MessageListenerConcurrently
package com.chris.rocketmq;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
/**
* @author Chris
* @date 2022-03-24 6:40 PM
*/
public class Consumer {
public static void main(String[] args) throws MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-consumer-group-001");
// Specify name server addresses.
consumer.setNamesrvAddr("master:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("topic-001", "*");
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
String msg_str = new String(msg.getBody());
System.out.println(msg_str);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
}

先启动conusmer1
再启动consumer2
最后启动生产者生产10条消息
conumser1消费结果
Consumer Started.
ConsumeMessageThread_1 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=198, queueOffset=25, sysFlag=0, bornTimestamp=1648123970750, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374262, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036768, commitLogOffset=223080, bodyCRC=35715212, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=26, CONSUME_START_TIME=1648123971053, UNIQ_KEY=7F000001445418B4AAC27AC8A8BD0000, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 48], transaction}]
Hello Chris 0
ConsumeMessageThread_2 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=198, queueOffset=26, sysFlag=0, bornTimestamp=1648123971007, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374472, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036A80, commitLogOffset=223872, bodyCRC=88947861, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=27, CONSUME_START_TIME=1648123971083, UNIQ_KEY=7F000001445418B4AAC27AC8A9BE0004, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 52], transaction}]
Hello Chris 4
ConsumeMessageThread_3 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=198, queueOffset=27, sysFlag=0, bornTimestamp=1648123971138, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374560, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036D98, commitLogOffset=224664, bodyCRC=217804990, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=28, CONSUME_START_TIME=1648123971162, UNIQ_KEY=7F000001445418B4AAC27AC8AA420008, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 56], transaction}]
Hello Chris 8
conumser2消费结果
ConsumeMessageThread_1 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=198, queueOffset=25, sysFlag=0, bornTimestamp=1648123970904, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374326, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000368F4, commitLogOffset=223476, bodyCRC=1814993312, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=26, CONSUME_START_TIME=1648123971089, UNIQ_KEY=7F000001445418B4AAC27AC8A9580002, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 50], transaction}]
Hello Chris 2
ConsumeMessageThread_2 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=198, queueOffset=25, sysFlag=0, bornTimestamp=1648123970885, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374306, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F000000000003682E, commitLogOffset=223278, bodyCRC=1965541402, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=26, CONSUME_START_TIME=1648123971117, UNIQ_KEY=7F000001445418B4AAC27AC8A9450001, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 49], transaction}]
Hello Chris 1
ConsumeMessageThread_3 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=198, queueOffset=26, sysFlag=0, bornTimestamp=1648123971100, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374527, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036C0C, commitLogOffset=224268, bodyCRC=1799577017, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=27, CONSUME_START_TIME=1648123971140, UNIQ_KEY=7F000001445418B4AAC27AC8AA1C0006, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 54], transaction}]
Hello Chris 6
ConsumeMessageThread_4 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=198, queueOffset=26, sysFlag=0, bornTimestamp=1648123971080, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374507, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036B46, commitLogOffset=224070, bodyCRC=1917455363, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=27, CONSUME_START_TIME=1648123971148, UNIQ_KEY=7F000001445418B4AAC27AC8AA080005, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 53], transaction}]
Hello Chris 5
ConsumeMessageThread_5 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=198, queueOffset=27, sysFlag=0, bornTimestamp=1648123971148, bornHost=/192.168.101.1:61608, storeTimestamp=1648123374575, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036E5E, commitLogOffset=224862, bodyCRC=2080129064, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=28, CONSUME_START_TIME=1648123971169, UNIQ_KEY=7F000001445418B4AAC27AC8AA4C0009, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 67, 104, 114, 105, 115, 32, 57], transaction}]
Hello Chris 9
创建消息者
在消费组
rocketmq-consumer-group-001启动两个消费者实例consumer1和consumer2然后再把消费组改为
rocketmq-consumer-group-002启动一个消费者实例consuemer3
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer1 = new DefaultMQPushConsumer("rocketmq-consumer-group-001");
DefaultMQPushConsumer consumer2 = new DefaultMQPushConsumer("rocketmq-consumer-group-001");
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer3 = new DefaultMQPushConsumer("rocketmq-consumer-group-002");
启动生产者,并向
topic-001中生产10条消息
consumer1消费结果:
Consumer Started.
ConsumeMessageThread_1 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413362, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813214, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000370AE, commitLogOffset=225454, bodyCRC=267036076, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413415, UNIQ_KEY=7F000001535C18B4AAC27ADEABF20002, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 50], transaction}]
Hello John 2
ConsumeMessageThread_2 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=197, queueOffset=27, sysFlag=0, bornTimestamp=1648125413408, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813248, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037173, commitLogOffset=225651, bodyCRC=2028836154, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=28, CONSUME_START_TIME=1648125413417, UNIQ_KEY=7F000001535C18B4AAC27ADEAC200003, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 51], transaction}]
Hello John 3
ConsumeMessageThread_3 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413438, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813282, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000373C2, commitLogOffset=226242, bodyCRC=143090101, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413468, UNIQ_KEY=7F000001535C18B4AAC27ADEAC3E0006, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 54], transaction}]
ConsumeMessageThread_4 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413447, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813294, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037487, commitLogOffset=226439, bodyCRC=2139115811, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413468, UNIQ_KEY=7F000001535C18B4AAC27ADEAC470007, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 55], transaction}]
Hello John 7
Hello John 6
consumer2消费结果:
Consumer Started.
ConsumeMessageThread_1 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=197, queueOffset=27, sysFlag=0, bornTimestamp=1648125413323, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813167, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036F24, commitLogOffset=225060, bodyCRC=1642382464, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=28, CONSUME_START_TIME=1648125413347, UNIQ_KEY=7F000001535C18B4AAC27ADEABCA0000, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 48], transaction}]
Hello John 0
ConsumeMessageThread_2 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413333, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813186, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036FE9, commitLogOffset=225257, bodyCRC=384037910, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413378, UNIQ_KEY=7F000001535C18B4AAC27ADEABD50001, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 49], transaction}]
Hello John 1
ConsumeMessageThread_3 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413413, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813256, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037238, commitLogOffset=225848, bodyCRC=1720254617, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413440, UNIQ_KEY=7F000001535C18B4AAC27ADEAC250004, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 52], transaction}]
Hello John 4
ConsumeMessageThread_4 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413431, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813272, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000372FD, commitLogOffset=226045, bodyCRC=294531087, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413447, UNIQ_KEY=7F000001535C18B4AAC27ADEAC370005, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 53], transaction}]
Hello John 5
ConsumeMessageThread_5 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413459, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813305, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F000000000003754C, commitLogOffset=226636, bodyCRC=1866419378, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413477, UNIQ_KEY=7F000001535C18B4AAC27ADEAC530008, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 56], transaction}]
Hello John 8
ConsumeMessageThread_6 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=30, sysFlag=0, bornTimestamp=1648125413470, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813310, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037611, commitLogOffset=226833, bodyCRC=406354980, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=31, CONSUME_START_TIME=1648125413494, UNIQ_KEY=7F000001535C18B4AAC27ADEAC5E0009, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 57], transaction}]
Hello John 9
consumer3消费结果:
Hello John 0
ConsumeMessageThread_13 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413333, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813186, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000036FE9, commitLogOffset=225257, bodyCRC=384037910, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413366, UNIQ_KEY=7F000001535C18B4AAC27ADEABD50001, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 49], transaction}]
Hello John 1
ConsumeMessageThread_9 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413362, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813214, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000370AE, commitLogOffset=225454, bodyCRC=267036076, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413407, UNIQ_KEY=7F000001535C18B4AAC27ADEABF20002, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 50], transaction}]
Hello John 2
ConsumeMessageThread_5 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=197, queueOffset=27, sysFlag=0, bornTimestamp=1648125413408, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813248, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037173, commitLogOffset=225651, bodyCRC=2028836154, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=28, CONSUME_START_TIME=1648125413419, UNIQ_KEY=7F000001535C18B4AAC27ADEAC200003, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 51], transaction}]
Hello John 3
ConsumeMessageThread_10 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413413, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813256, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037238, commitLogOffset=225848, bodyCRC=1720254617, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413435, UNIQ_KEY=7F000001535C18B4AAC27ADEAC250004, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 52], transaction}]
Hello John 4
ConsumeMessageThread_8 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413431, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813272, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000372FD, commitLogOffset=226045, bodyCRC=294531087, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413443, UNIQ_KEY=7F000001535C18B4AAC27ADEAC370005, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 53], transaction}]
Hello John 5
ConsumeMessageThread_7 Receive New Messages: MessageExt [brokerName=master, queueId=0, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413438, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813282, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F00000000000373C2, commitLogOffset=226242, bodyCRC=143090101, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413460, UNIQ_KEY=7F000001535C18B4AAC27ADEAC3E0006, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 54], transaction}]
Hello John 6
ConsumeMessageThread_6 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=197, queueOffset=28, sysFlag=0, bornTimestamp=1648125413447, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813294, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037487, commitLogOffset=226439, bodyCRC=2139115811, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=29, CONSUME_START_TIME=1648125413464, UNIQ_KEY=7F000001535C18B4AAC27ADEAC470007, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 55], transaction}]
Hello John 7
ConsumeMessageThread_19 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=197, queueOffset=29, sysFlag=0, bornTimestamp=1648125413459, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813305, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F000000000003754C, commitLogOffset=226636, bodyCRC=1866419378, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=30, CONSUME_START_TIME=1648125413474, UNIQ_KEY=7F000001535C18B4AAC27ADEAC530008, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 56], transaction}]
Hello John 8
ConsumeMessageThread_1 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=197, queueOffset=30, sysFlag=0, bornTimestamp=1648125413470, bornHost=/192.168.101.1:61991, storeTimestamp=1648124813310, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000037611, commitLogOffset=226833, bodyCRC=406354980, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-001', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=31, CONSUME_START_TIME=1648125413492, UNIQ_KEY=7F000001535C18B4AAC27ADEAC5E0009, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 74, 111, 104, 110, 32, 57], transaction}]
Hello John 9
结论
可以看出consumer1和consumer2分摊
topic-001中的消息, 即两个消费者所消费的消息总和为10条而consumer3将
topic-001中的消息全部消息掉
集群模式和广播模式默认集群模式
如果改为广播模式,即使是同一组的不同消费者也会消费到生产者生产的全部消息
consumer.setMessageModel(MessageModel.BROADCASTING);
/**
* broadcast
*/
BROADCASTING("BROADCASTING"),
/**
* clustering
*/
CLUSTERING("CLUSTERING");
SendResult sendResult = producer.send(msg);
//发送同步消息
SendResult sendResult = producer.send(msg);
producer.send(msg, new SendCallback()
package com.chris.rocketmq;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CountDownLatch;
/**
* @author Chris
* @date 2022-03-24 6:35 PM
*/
public class ProducerAsync {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException,
InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-producer-group-001");
producer.setNamesrvAddr("master:9876");
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
System.out.println("producer has started!");
int messageCount = 10;
CountDownLatch countDownLatch = new CountDownLatch(messageCount);
for (int i = 0; i < messageCount; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-003" /* Topic */, "TagA" /* Tag */,
("Hello Async " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
//Send Messages Asynchronously
//Asynchronous transmission is generally used in response time sensitive business scenarios.
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%s%n", sendResult);
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.println("exception:" + e.getMessage());
}
});
}
countDownLatch.await();
producer.shutdown();
}
}
不需要有回执的消息,比如说日志类消息
producer.sendOneway(msg);
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException,
InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-producer-group-001");
producer.setNamesrvAddr("master:9876");
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
System.out.println("producer has started!");
int messageCount = 10;
for (int i = 0; i < messageCount; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-005" /* Topic */, "TagA" /* Tag */,
("Hello OneWay " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
producer.sendOneway(msg);
}
producer.shutdown();
}
消息发送时并不直接发送到消息服务器,而是根据设定的时间到达,起到延时到达的缓冲作用
msg.setDelayTimeLevel(3);
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
producer
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException,
InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-producer-group-001");
producer.setNamesrvAddr("master:9876");
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
System.out.println("producer has started!");
int messageCount = 10;
for (int i = 0; i < messageCount; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-005" /* Topic */, "TagA" /* Tag */,
("Hello DealyTime " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
consumer
public static void main(String[] args) throws MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-consumer-group-003");
// Specify name server addresses.
consumer.setNamesrvAddr("master:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("topic-005", "*");
// 默认早集群模式,如果改为广播模式,同组的不同消费将会消费到生产者生产的全部消息
consumer.setMessageModel(MessageModel.BROADCASTING);
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
String msg_str = new String(msg.getBody());
// Print approximate delay time period
System.out.println("Receive message[msghljs-string" style="color: #d69d85; line-height: 160%; box-sizing: content-box;">",msg_str=] " + msg_str + "," + (System.currentTimeMillis() - msg.getStoreTimestamp()) + "ms later");
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
一次发送多条消息,节约网络开销
注意:
这些批量消息应该有相同的topic
相同的waitStoreMsgOK
不能是延时消息
消息内容总长度不超过4M
Besides, the total size of the messages in one batch should be no more than 1MiB.消息内容总长度包含如下:
topic 字符串字节数
body字节数组长度
消息追加属性(key与value对应字符串字节数)
日志(固定20字节)
List<Message> msgList = CollUtil.newArrayList();
for (int i = 0; i < 10; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-006" /* Topic */, "TagA" /* Tag */,
("Hello Message in Batch " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
msgList.add(msg);
}
//批量发送同步消息
SendResult sendResult = producer.send(msgList);
SendResult [sendStatus=SEND_OK, msgId=7F00000143AC18B4AAC27F0FDDBA0000,7F00000143AC18B4AAC27F0FDDBA0001,7F00000143AC18B4AAC27F0FDDBA0002,7F00000143AC18B4AAC27F0FDDBA0003,7F00000143AC18B4AAC27F0FDDBA0004,7F00000143AC18B4AAC27F0FDDBB0005,7F00000143AC18B4AAC27F0FDDBB0006,7F00000143AC18B4AAC27F0FDDBB0007,7F00000143AC18B4AAC27F0FDDBB0008,7F00000143AC18B4AAC27F0FDDBB0009, offsetMsgId=C0A8657F00002A9F000000000003F28C,C0A8657F00002A9F000000000003F367,C0A8657F00002A9F000000000003F442,C0A8657F00002A9F000000000003F51D,C0A8657F00002A9F000000000003F5F8,C0A8657F00002A9F000000000003F6D3,C0A8657F00002A9F000000000003F7AE,C0A8657F00002A9F000000000003F889,C0A8657F00002A9F000000000003F964,C0A8657F00002A9F000000000003FA3F, messageQueue=MessageQueue [topic=topic-006, brokerName=master, queueId=0], queueOffset=0]
生产者分别生产 tag类型为 customer 和 vip的消息
for (int i = 0; i < 10; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-006" /* Topic */, "customer" /* Tag */,
("Hello vip " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
//Call send message to deliver message to one of brokers.
//发送同步消息
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
for (int i = 0; i < 10; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-006" /* Topic */, "vip" /* Tag */,
("Hello vip " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
//Call send message to deliver message to one of brokers.
//发送同步消息
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消息者分别订阅tag为customer和 tag为 customer || vip的消息
consumer1.subscribe("topic-006", "customer");
consumer2.subscribe("topic-006", "customer || vip");
结果:
consumer1 和 consumer2可以分别消费到tag=customer类型的不同消息,总数为customer类型消息的总和
consumer2除了可以消费到customer的消息外,还可以消费到全部的tag=vip的消息
数值比较:
>,>=,<,<=,BETWEEN,=字符比较:
=,<>,IN
IS NULLorIS NOT NULL;逻辑比较:
AND,OR,NOT;常量支持类型为
- 数值, 比如 123, 3.1415;
- 字符,比如 ‘abc’, 必须用单引号引用;
NULL, 特殊的常量- 布尔值,
TRUE或FALSE;
/opt/rocketmq/conf
vi broker.conf
enablePropertyFilter=true
修改完成后重启namerserver和broker
sh bin/mqshutdown broker
sh bin/mqshutdown namesrv
nohup sh bin/mqnamesrv &
nohup sh bin/mqbroker -n master:9876 &
查看集群配置属性

如果值没有变更为true 需要执行如下命令
[root@master rocketmq]# sh bin/mqadmin updateBrokerConfig -b master:10911 -k enablePropertyFilter -v true
RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.PlatformDependent0).
RocketMQLog:WARN Please initialize the logger system properly.
update broker config success, master:10911

生产者
msg.putUserProperty("vip", String.valueOf(i));
for (int i = 0; i < 10; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("topic-006" /* Topic */, "vip" /* Tag */,
("Hello vip " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
msg.putUserProperty("vip", String.valueOf(i));
msg.putUserProperty("number", String.valueOf(i));
//Call send message to deliver message to one of brokers.
//发送同步消息
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消息者
MessageSelector.bySql("number between 3 and 7 and vip > 4")
// only subsribe messages have property vip and number, also number between 3 and 7 and vip > 4
consumer.subscribe("topic-006", MessageSelector.bySql("number between 3 and 7 and vip > 4"));
ConsumeMessageThread_2 Receive New Messages: MessageExt [brokerName=master, queueId=1, storeSize=210, queueOffset=19, sysFlag=0, bornTimestamp=1648205009964, bornHost=/192.168.101.1:53854, storeTimestamp=1648204988448, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000043BAC, commitLogOffset=277420, bodyCRC=1358519611, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-006', flag=0, properties={MIN_OFFSET=0, number=5, MAX_OFFSET=20, CONSUME_START_TIME=1648205011754, vip=5, UNIQ_KEY=7F0000013D9018B4AAC27F9D382B0005, CLUSTER=DefaultCluster, WAIT=true, TAGS=vip}, body=[72, 101, 108, 108, 111, 32, 118, 105, 112, 32, 53], transaction}]
Hello vip 5
ConsumeMessageThread_3 Receive New Messages: MessageExt [brokerName=master, queueId=2, storeSize=210, queueOffset=26, sysFlag=0, bornTimestamp=1648205011737, bornHost=/192.168.101.1:53854, storeTimestamp=1648204988466, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000043C7E, commitLogOffset=277630, bodyCRC=1240468609, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-006', flag=0, properties={MIN_OFFSET=0, number=6, MAX_OFFSET=27, CONSUME_START_TIME=1648205012407, vip=6, UNIQ_KEY=7F0000013D9018B4AAC27F9D3F190006, CLUSTER=DefaultCluster, WAIT=true, TAGS=vip}, body=[72, 101, 108, 108, 111, 32, 118, 105, 112, 32, 54], transaction}]
Hello vip 6
ConsumeMessageThread_4 Receive New Messages: MessageExt [brokerName=master, queueId=3, storeSize=210, queueOffset=16, sysFlag=0, bornTimestamp=1648205011752, bornHost=/192.168.101.1:53854, storeTimestamp=1648204988477, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F0000000000043D50, commitLogOffset=277840, bodyCRC=1056390167, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='topic-006', flag=0, properties={MIN_OFFSET=0, number=7, MAX_OFFSET=17, CONSUME_START_TIME=1648205012500, vip=7, UNIQ_KEY=7F0000013D9018B4AAC27F9D3F280007, CLUSTER=DefaultCluster, WAIT=true, TAGS=vip}, body=[72, 101, 108, 108, 111, 32, 118, 105, 112, 32, 55], transaction}]
Hello vip 7
http://localhost:8081/rocketmq/send/sendMessage
//存入rocketmq中的结果
{"name":"Chris","age":32}
建module
rocketmqcloud2022
改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
写yml
spring:
application:
name: rocketmqcloud2022
server:
port: 8081
servlet:
encoding:
charset: UTF-8
context-path: /rocketmq
tomcat:
uri-encoding: UTF-8
rocketmq:
name-server: master:9876
producer:
group: rocketmq-producer-group-001
生产者
@RestController
@RequestMapping("/send")
public class SendController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@PostMapping("/sendMessage")
public String sendMsg(@RequestBody @Validated User user) {
rocketMQTemplate.convertAndSend("cloud001", user);
return "success";
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
@NotNull(message = "name can't be null")
private String name;
@Range(max = 100, min = 1, message = "age between 1 and 100")
private Integer age;
}
消费者
RocketMQListener
@RocketMQMessageListener
package com.chris.rocketmq.rocketmqcloud2022.service;
import cn.hutool.json.JSONUtil;
import com.chris.rocketmq.rocketmqcloud2022.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "cloud-consume-001",topic = "cloud001",
selectorType = SelectorType.TAG, selectorExpression = "*")
public class UserConsumer implements RocketMQListener<User> {
@Override
public void onMessage(User user) {
log.info("rocketmq msg:{}", JSONUtil.toJsonStr(user));
}
}
@PostMapping("/sendOrderMessage")
public SendResult sendOrderMessage() {
List<OrderInfo> orderInfos = CollUtil.newArrayList();
orderInfos.add(new OrderInfo(1L, "create"));
orderInfos.add(new OrderInfo(2L, "create"));
orderInfos.add(new OrderInfo(1L, "send"));
orderInfos.add(new OrderInfo(3L, "create"));
orderInfos.add(new OrderInfo(2L, "send"));
orderInfos.add(new OrderInfo(4L, "create"));
orderInfos.add(new OrderInfo(1L, "pay"));
orderInfos.add(new OrderInfo(1L, "finish"));
orderInfos.add(new OrderInfo(3L, "send"));
orderInfos.add(new OrderInfo(2L, "pay"));
orderInfos.add(new OrderInfo(4L, "send"));
orderInfos.add(new OrderInfo(3L, "pay"));
orderInfos.add(new OrderInfo(2L, "finish"));
orderInfos.add(new OrderInfo(4L, "pay"));
orderInfos.add(new OrderInfo(3L, "finish"));
orderInfos.add(new OrderInfo(4L, "finish"));
List<Message<OrderInfo>> messageList = CollUtil.newArrayList();
for (OrderInfo orderInfo : orderInfos) {
Message<OrderInfo> message = MessageBuilder.withPayload(orderInfo).build();
messageList.add(message);
}
SendResult sendResult = rocketMQTemplate.syncSend(String.join(":", topic, order_tag), messageList, 10000);
log.info("sendOrderMessage sendResult:{}", sendResult);
return sendResult;
}
2022-03-27 11:57:17.531 [MessageThread_8] : consume orderInfo msg:{"orderId":1,"desc":"finish"}
2022-03-27 11:57:17.531 [MessageThread_1] : consume orderInfo msg:{"orderId":1,"desc":"create"}
2022-03-27 11:57:17.531 [MessageThread_5] : consume orderInfo msg:{"orderId":2,"desc":"send"}
2022-03-27 11:57:17.531 [essageThread_13] : consume orderInfo msg:{"orderId":2,"desc":"finish"}
2022-03-27 11:57:17.531 [essageThread_16] : consume orderInfo msg:{"orderId":4,"desc":"finish"}
2022-03-27 11:57:17.531 [essageThread_11] : consume orderInfo msg:{"orderId":4,"desc":"send"}
2022-03-27 11:57:17.531 [MessageThread_7] : consume orderInfo msg:{"orderId":1,"desc":"pay"}
2022-03-27 11:57:17.532 [essageThread_14] : consume orderInfo msg:{"orderId":4,"desc":"pay"}
2022-03-27 11:57:17.532 [MessageThread_3] : consume orderInfo msg:{"orderId":1,"desc":"send"}
2022-03-27 11:57:17.532 [MessageThread_6] : consume orderInfo msg:{"orderId":4,"desc":"create"}
2022-03-27 11:57:17.532 [MessageThread_4] : consume orderInfo msg:{"orderId":3,"desc":"create"}
2022-03-27 11:57:17.532 [essageThread_15] : consume orderInfo msg:{"orderId":3,"desc":"finish"}
2022-03-27 11:57:17.532 [MessageThread_9] : consume orderInfo msg:{"orderId":3,"desc":"send"}
2022-03-27 11:57:17.533 [MessageThread_2] : consume orderInfo msg:{"orderId":2,"desc":"create"}
2022-03-27 11:57:17.533 [essageThread_12] : consume orderInfo msg:{"orderId":3,"desc":"pay"}
2022-03-27 11:57:17.533 [essageThread_10] : consume orderInfo msg:{"orderId":2,"desc":"pay"}
生产者
new MessageQueueSelector()
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException,
InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-producer-group-002");
producer.setNamesrvAddr("master:9876");
producer.start();
System.out.println("producer has started!");
List<OrderInfo> orderInfos = getOrderInfos();
for (OrderInfo orderInfo : orderInfos) {
Message message = new Message("topic-008", "order", orderInfo.toString().getBytes(StandardCharsets.UTF_8));
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
System.out.println("message queue selector, arg:" + arg);
int size = mqs.size();
long id = (long) arg;
long queue_inx = id % size;
return mqs.get(new Long(queue_inx).intValue());
}
}, orderInfo.getId(), 10000);
System.out.println("send result:" + sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
private static List<OrderInfo> getOrderInfos() {
List<OrderInfo> orderInfos = CollUtil.newArrayList();
orderInfos.add(new OrderInfo(1L, "create"));
orderInfos.add(new OrderInfo(2L, "create"));
orderInfos.add(new OrderInfo(1L, "send"));
orderInfos.add(new OrderInfo(3L, "create"));
orderInfos.add(new OrderInfo(2L, "send"));
orderInfos.add(new OrderInfo(4L, "create"));
orderInfos.add(new OrderInfo(1L, "pay"));
orderInfos.add(new OrderInfo(1L, "finish"));
orderInfos.add(new OrderInfo(3L, "send"));
orderInfos.add(new OrderInfo(2L, "pay"));
orderInfos.add(new OrderInfo(4L, "send"));
orderInfos.add(new OrderInfo(3L, "pay"));
orderInfos.add(new OrderInfo(2L, "finish"));
orderInfos.add(new OrderInfo(4L, "pay"));
orderInfos.add(new OrderInfo(3L, "finish"));
orderInfos.add(new OrderInfo(4L, "finish"));
return orderInfos;
}
message queue selector, arg:1
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9350000, offsetMsgId=C0A8657F00002A9F0000000000058C95, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=1], queueOffset=12]
message queue selector, arg:2
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B93F0001, offsetMsgId=C0A8657F00002A9F0000000000058D6B, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=2], queueOffset=12]
message queue selector, arg:1
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9430002, offsetMsgId=C0A8657F00002A9F0000000000058E41, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=1], queueOffset=13]
message queue selector, arg:3
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9460003, offsetMsgId=C0A8657F00002A9F0000000000058F15, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=3], queueOffset=12]
message queue selector, arg:2
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9600004, offsetMsgId=C0A8657F00002A9F0000000000058FEB, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=2], queueOffset=13]
message queue selector, arg:4
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9640005, offsetMsgId=C0A8657F00002A9F00000000000590BF, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=0], queueOffset=12]
message queue selector, arg:1
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9690006, offsetMsgId=C0A8657F00002A9F0000000000059195, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=1], queueOffset=14]
message queue selector, arg:1
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B96E0007, offsetMsgId=C0A8657F00002A9F0000000000059268, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=1], queueOffset=15]
message queue selector, arg:3
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9720008, offsetMsgId=C0A8657F00002A9F000000000005933E, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=3], queueOffset=13]
message queue selector, arg:2
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9770009, offsetMsgId=C0A8657F00002A9F0000000000059412, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=2], queueOffset=14]
message queue selector, arg:4
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B986000A, offsetMsgId=C0A8657F00002A9F00000000000594E5, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=0], queueOffset=13]
message queue selector, arg:3
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B999000B, offsetMsgId=C0A8657F00002A9F00000000000595B9, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=3], queueOffset=14]
message queue selector, arg:2
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B99D000C, offsetMsgId=C0A8657F00002A9F000000000005968C, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=2], queueOffset=15]
message queue selector, arg:4
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9A2000D, offsetMsgId=C0A8657F00002A9F0000000000059762, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=0], queueOffset=14]
message queue selector, arg:3
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9AB000E, offsetMsgId=C0A8657F00002A9F0000000000059835, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=3], queueOffset=15]
message queue selector, arg:4
send result:SendResult [sendStatus=SEND_OK, msgId=7F000001159418B4AAC289A4B9AE000F, offsetMsgId=C0A8657F00002A9F000000000005990B, messageQueue=MessageQueue [topic=topic-008, brokerName=master, queueId=0], queueOffset=15]
消息者
new MessageListenerOrderly()
// Register MessageListenerOrderly to execute on arrival of messages fetched from brokers, receive messages
// orderly One queue by one thread.
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
for (MessageExt msg : msgs) {
String msg_str = new String(msg.getBody());
System.out.println(msg_str);
}
return ConsumeOrderlyStatus.SUCCESS;
});
可以看出消息都是从
create - send - pay - finish这几个状态顺序消费
OrderInfo(id=1, desc=create)
OrderInfo(id=2, desc=create)
OrderInfo(id=1, desc=send)
OrderInfo(id=3, desc=create)
OrderInfo(id=2, desc=send)
OrderInfo(id=4, desc=create)
OrderInfo(id=1, desc=pay)
OrderInfo(id=1, desc=finish)
OrderInfo(id=3, desc=send)
OrderInfo(id=2, desc=pay)
OrderInfo(id=4, desc=send)
OrderInfo(id=3, desc=pay)
OrderInfo(id=2, desc=finish)
OrderInfo(id=4, desc=pay)
OrderInfo(id=3, desc=finish)
OrderInfo(id=4, desc=finish)
生产者
@PostMapping("/sendOrderMessageOrderly")
public String sendOrderMessageOrderly() {
List<OrderInfo> orderInfos = getOrderInfos();
for (OrderInfo orderInfo : orderInfos) {
Message<OrderInfo> message = MessageBuilder.withPayload(orderInfo).build();
SendResult sendResult = rocketMQTemplate.syncSendOrderly(String.join(":", topic, order_tag), message, String.valueOf(orderInfo.getOrderId()));
log.info("send orderly result:{}", sendResult);
}
return "success";
}
消息者
consumeMode = ConsumeMode.ORDERLY
@Service
@Slf4j
@RocketMQMessageListener(topic = "cloud006", selectorType = SelectorType.TAG, selectorExpression = "order",
consumerGroup = "cloud-consume-group-006", consumeMode = ConsumeMode.ORDERLY)
public class OrderConsumer implements RocketMQListener<OrderInfo> {
@Override
public void onMessage(OrderInfo orderInfo) {
log.info("consume orderInfo msg:{}", JSONUtil.toJsonStr(orderInfo));
}
}
@Service
@Slf4j
@RocketMQMessageListener(topic = "cloud006", selectorType = SelectorType.TAG, selectorExpression = "order",
consumerGroup = "cloud-consume-group-006", consumeMode = ConsumeMode.ORDERLY)
public class OrderConsumer2 implements RocketMQListener<OrderInfo> {
@Override
public void onMessage(OrderInfo orderInfo) {
log.info("consume orderInfo msg:{}", JSONUtil.toJsonStr(orderInfo));
}
}
可以看出如果启动两个消费者
consumer和consumer2则
consumer和consumer2按顺序分别消息topic cloud006里面的消息
[MessageThread_2] OrderConsumer2 : consume orderInfo msg:{"orderId":2,"desc":"create"}
[MessageThread_1] OrderConsumer2 : consume orderInfo msg:{"orderId":3,"desc":"create"}
[MessageThread_2] OrderConsumer2 : consume orderInfo msg:{"orderId":2,"desc":"send"}
[MessageThread_1] OrderConsumer2 : consume orderInfo msg:{"orderId":3,"desc":"send"}
[MessageThread_1] OrderConsumer2 : consume orderInfo msg:{"orderId":3,"desc":"pay"}
[MessageThread_2] OrderConsumer2 : consume orderInfo msg:{"orderId":2,"desc":"pay"}
[MessageThread_1] OrderConsumer2 : consume orderInfo msg:{"orderId":3,"desc":"finish"}
[MessageThread_2] OrderConsumer2 : consume orderInfo msg:{"orderId":2,"desc":"finish"}
[MessageThread_1] OrderConsumer : consume orderInfo msg:{"orderId":4,"desc":"create"}
[MessageThread_1] OrderConsumer : consume orderInfo msg:{"orderId":4,"desc":"send"}
[MessageThread_1] OrderConsumer : consume orderInfo msg:{"orderId":4,"desc":"pay"}
[MessageThread_1] OrderConsumer : consume orderInfo msg:{"orderId":4,"desc":"finish"}
[MessageThread_2] OrderConsumer : consume orderInfo msg:{"orderId":1,"desc":"create"}
[MessageThread_2] OrderConsumer : consume orderInfo msg:{"orderId":1,"desc":"send"}
[MessageThread_2] OrderConsumer : consume orderInfo msg:{"orderId":1,"desc":"pay"}
[MessageThread_2] OrderConsumer : consume orderInfo msg:{"orderId":1,"desc":"finish"}
| 创建时间: | 2020/9/2 15:24 |
| 更新时间: | 2022/3/29 22:52 |
| 作者: | Chris |
安装
sudo -s
apt-get install git
git help config


配置文件就是Git全局配置的文件,一般配置方法是git config --global <配置名称> <配置的值>
git config --global user.name "chris"
git config --global user.email "lilunlogic@163.com"
会在家目录(/home/chris)下建立一个叫.gitconfig 的文件(该文件为隐藏文件,需要使用ls -al查看到)
git config --list
git clone https://github.com/ApeTogether/DemoTest
mkdir repo1
cd repo1
git init 或 git init --bare
Initialized empty Git repository in /home/chris/githome/repo1/.git/
touch file1 file2 file3
echo "test file" >> file1
echo "test file" >> file2
echo "test file" >> file3
此时可以使用git status命令查看当前git仓库的状态
git status
使用git add命令添加新创建或修改的文件到本地的缓存区(Index)
git add file1 file2 file 3
仅监控已经被add的文件(即tracked file),会将被修改的文件提交到暂存区。add -u 不会提交新文件(untracked file)
git add -u
把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件
git add .
是上面两个功能的合集
git add -A
git reset
This form resets the index entries for all <paths> to their state at <tree-ish>. (It does not affect the working tree or the current branch.)
This means that git reset <paths> is the opposite of git add <paths>.
使用 git diff 命令再加上 --cached 参数,看看缓存区中哪些文件被修改了。
进入到git diff --cached界面后需要输入q才可以退出:
git diff --cached
如果没有--cached参数,git diff 会显示当前你所有已做的但没有加入到索引里的修改
使用git commit命令提交到本地代码库, 需要使用-m添加本次修改的注释
git commit -m "add 3 files"
[master (root-commit) 054c93f] add 3 files
3 files changed, 5 insertions(+)
create mode 100644 file1
create mode 100644 file2
create mode 100644 file3
从暂存区移除文件
git rm --cached filename
强制从工作区和暂存区移除文件
git rm -f filename
如果没有指定快照名字则从暂存区检出文件到工作区
git checkout -- filename
如果指定了快照名字则从仓库检出文件到暂存区和工作区
git checkout HEAD~ filename
除了用git add 命令,我们还可以用下面的命令将所有没有加到缓存区的修改也一起提交,但-a命令不会添加新建的文件。
git commit -am "add 3 files"
修改最新一次提交时的批注信息
git commit --amend
git commit --amend -m "fix the comment for last submittion"
如果是删除文件,则直接使用git rm命令删除后会自动将已删除文件的信息添加到缓存区,
git commit提交后就会将本地仓库中的对应文件删除
只删除工作区和暂存区的文件,也就是取消跟踪.
git rm file3
将file1的名字改为file2
git mv file1 file2
移动HEAD的指向将其指向上一个版本并将上一个版本回滚到暂存区
git reset [--mixed] HEAD~
移动HEAD的指向将其指向上一个版本并将上上一个版本回滚到暂存区
git reset HEAD~2
移动HEAD的指向将其指向上一个版本,相当于回滚了一次错误的commit
git reset --soft HEAD~
回滚到指定版本
git reset 753e10 即可以回滚又可以向前滚,但只滚到暂存区
git reset --hard 753e10 一步回滚到仓库区
将HEAD指向的分支及HEAD本身切到目标分支
git reset -- hard branchName
创建分支
git branch branchName
创建并切换到新分支
git checkout -b branchName
切换分支f
git checkout branchName
1:修改快照指针
2:将当前快照的内容还原到工作区和暂存区
git checkout branchId
或
git checkout HEAD~
git log --decorate --oneline --graph --all
git merge branchName
比较两个不同版本
git diff versionId1 versionId2
比较工作区与某一版本
git diff versionId
比较工作区和不同版本
git diff HEAD
要查看当前配置有哪些远程仓库
git remote -v
在克隆完某个项目后,至少可以看到一个名为 origin 的远程库,Git 默认使用这个名字来标识你所克隆的原始仓库.
将本地仓库关联到远端服务器,我们可以使用 git remote 命令,不同于刚刚的 git clone 命令,直接将远端的仓库克隆下来。 我们当前的仓库是使用 git init 初始化的本地仓库,所以我们需要将本地仓库与远程仓库关联,使用如下命令(需要修改下面的远程仓库地址为自己的仓库地址):
git remote add origin https://github.com/ApeTogether/repo1.git
git remote add origin git@github.com:ApeTogether/repo1.git
git push -u origin master
git push -u origin branch_001
这个时候如果本地的仓库连接到了远程Git服务器,可以使用下面的命令将本地仓库同步到远端服务器:
# 需要输入仓库对应的用户名和密码
$ git push -u origin master
git push https://github.com/ApeTogether/RepoTest01.git master
## create a new repository on the command line
echo "# RepoTest01" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:ApeTogether/RepoTest01.git
git push -u origin master
## push an existing repository from the command line
git remote add origin git@github.com:ApeTogether/RepoTest01.git
git push -u origin master
通过.gitignore将文件不纳入版本控制
touch .gitignore
index.*
.gitignore
*匹配零个或任意多个字符
[abc]匹配任意一下在括号内的字符
?只匹配一个任意字符
[0-9][a-z]匹配范围
git help git-command
cd "D:\AI_Project\final-report-service"
git log
git status
git stash == git stash push
git stash list
git stash show
git stash pop
git checkout [branch name]
git pull
delete a local branch
git branch [-d or -D] develop
If during the merge you get a conflict, the best way to undo the merge is:
git merge --abort
merge the hotfix branch back into your master branch
$ git checkout master
$ git checkout -b hotfix origin/hotfix [将远程hotfix拉到本地并检出]
$ git merge hotfix
git push <remote> localbranch:remotebranch
$git push origin master:master
在local repository中找到名字为master的branch,使用它去更新remote repository下名字为master的branch,如果remote repository下不存在名字是master的branch,那么新建一个
冒号前表示local branch的名字,冒号后表示remote repository下 branch的名字。注意,如果你省略了<dst>,git就认为你想push到remote repository下和local branch相同名字的branch
关联之后再执行git pull, git push操作时就不需要指定对应的远程分支
git push --set-upstream origin CFT-73 == git push -u origin CFT-73
forch push the commits into remote branch FU-382
git push -f -u origin FU-382
you get a far more simplified output from the command
git status -s
unstaging the staged file
git reset HEAD CONTRIBUTING.md
unmodifying a Modified File
git checkout -- CONTRIBUTING.md
which shows you the URLs that Git has stored for the shortname to be used when reading and writing to that remote
$ git remote -v origin https://github.com/schacon/ticgit (fetch) origin https://github.com/schacon/ticgit (push)
git fetch orgin master //将远程仓库的master分支下载到本地当前branch中
the git fetch origin command only downloads the data to your local repository — it doesn’t automatically merge it with any of your work or modify what you’re currently working on
You have to merge it manually into your work when you’re ready.
change the remtoe url in local repository
git remote set-url origin git://new.url.here
git remote set-url origin https://github.com/ChrisLi716/myhadoopdemo.git
git config --global credential.helper store
javax.validation的一系列注解可以帮我们完成参数校验,免去繁琐的串行校验
javax.validation
JSR303是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面(面向注解编程的时代),就可以在需要校验的时候进行校验了
javax.validation在SpringBoot中已经包含在starter-web中
在其他项目中可以引用依赖,并自行调整版本:
<!--jsr 303-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>


此处只列出Hibernate Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。
@Validated 声明要检查的参数/**
* 走参数校验注解
*
* @param userDTO
* @return
*/
@PostMapping("/save/valid")
public RspDTO save(@RequestBody @Validated UserDTO userDTO) {
userService.save(userDTO);
return RspDTO.success();
}
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;
@Data
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
/*** 用户ID*/
@NotNull(message = "用户id不能为空")
private Long userId;
/** 用户名*/
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过20个字符")
@Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
private String username;
/** 手机号*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String mobile;
/**性别*/
private String sex;
/** 邮箱*/
@NotBlank(message = "联系邮箱不能为空")
@Email(message = "邮箱格式不对")
private String email;
/** 密码*/
private String password;
/*** 创建时间 */
@Future(message = "时间必须是将来时间")
private Date createTime;
}
MethodArgumentNotValidException是springBoot中进行绑定参数校验时的异常,需要在springBoot中处理
其他需要处理ConstraintViolationException异常进行处理.
import com.boot.lea.mybot.dto.RspDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
/**
* @author Lilun
* @ClassName: GlobalExceptionHandler
* @Description: 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
private static int DUPLICATE_KEY_CODE = 1001;
private static int PARAM_FAIL_CODE = 1002;
private static int VALIDATION_CODE = 1003;
/**
* 处理自定义异常
*/
@ExceptionHandler(BizException.class)
public RspDTO handleRRException(BizException e) {
logger.error(e.getMessage(), e);
return new RspDTO(e.getCode(), e.getMessage());
}
/**
* 方法参数校验
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public RspDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error(e.getMessage(), e);
return new RspDTO(PARAM_FAIL_CODE, e.getBindingResult().getFieldError().getDefaultMessage());
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVo<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
logger.error("参数错误,异常信息->{}", ex.getMessage());
String message = ex.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return fail(HttpStatus.BAD_REQUEST.value(), message, ex);
}
/**
* ValidationException
*/
@ExceptionHandler(ValidationException.class)
public RspDTO handleValidationException(ValidationException e) {
logger.error(e.getMessage(), e);
return new RspDTO(VALIDATION_CODE, e.getCause().getMessage());
}
/**
* ConstraintViolationException
*/
@ExceptionHandler(ConstraintViolationException.class)
public RspDTO handleConstraintViolationException(ConstraintViolationException e) {
logger.error(e.getMessage(), e);
return new RspDTO(PARAM_FAIL_CODE, e.getMessage());
}
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVo<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
logger.error("参数错误,异常信息->{}", ex.getMessage());
String message = ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
return fail(HttpStatus.BAD_REQUEST.value(), message, ex);
}
@ExceptionHandler(NoHandlerFoundException.class)
public RspDTO handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return new RspDTO(404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DuplicateKeyException.class)
public RspDTO handleDuplicateKeyException(DuplicateKeyException e) {
logger.error(e.getMessage(), e);
return new RspDTO(DUPLICATE_KEY_CODE, "数据重复,请检查后提交");
}
@ExceptionHandler(Exception.class)
public RspDTO handleException(Exception e) {
logger.error(e.getMessage(), e);
return new RspDTO(500, "系统繁忙,请稍后再试");
}
}
在ValidationMessages.properties 就是校验的message,有着已经写好的默认的message,且是支持i18n的,大家可以阅读源码赏析
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {
String message() default "身份证号码不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这个注解是作用在Field字段上,运行时生效,
触发的是IdentityCardNumber这个验证类。
- message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
- groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
- payload 主要是针对bean的,使用不多。
这个是真正进行验证的逻辑代码
public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
@Override
public void initialize(IdentityCardNumber identityCardNumber) {
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
return IdCardValidatorUtils.isValidate18Idcard(o.toString());
}
}
@NotBlank(message = "身份证号不能为空")
@IdentityCardNumber(message = "身份证信息有误,请核对后提交")
private String clientCardNo;
如果同一个对象要复用,比如
UserDTO在更新时候要校验userId,在保存的时候不需要校验userId,在两种情况下都要校验username,那就用上groups了
先定义groups的分组接口Create和Update
import javax.validation.groups.Default;
public interface Create extends Default {
}
import javax.validation.groups.Default;
public interface Update extends Default{
}
再在需要校验的地方
@Validated声明校验组
/**
* 走参数校验注解的 groups 组合校验
*
* @param userDTO
* @return
*/
@PostMapping("/update/groups")
public RspDTO update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
userService.updateById(userDTO);
return RspDTO.success();
}
在DTO中的字段上定义好groups = {}的分组类型
@Data
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
/*** 用户ID*/
@NotNull(message = "用户id不能为空", groups = Update.class)
private Long userId;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过20个字符", groups = {Create.class, Update.class})
@Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
private String username;
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误", groups = {Create.class, Update.class})
private String mobile;
/**
* 性别
*/
private String sex;
/**
* 邮箱
*/
@NotBlank(message = "联系邮箱不能为空")
@Email(message = "邮箱格式不对")
private String email;
/**
* 密码
*/
private String password;
/*** 创建时间 */
@Future(message = "时间必须是将来时间", groups = {Create.class})
private Date createTime;
}
注意:在声明分组的时候尽量加上
extend javax.validation.groups.Default
否则,在你声明@Validated(Update.class)的时候,就会出现你在默认没添加groups = {} 的时候的校验组@Email(message = "邮箱格式不对"), 会不去校验, 因为默认的校验组是groups = {Default.class}.
在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated
@RestController
@RequestMapping("user/")
@Validated
public class UserController extends AbstractController {
@GetMapping("/get")
public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) {
User user = userService.selectById(userId);
if (user == null) {
return new RspDTO<User>().nonAbsent("用户不存在");
}
return new RspDTO<User>().success(user);
}
}
| 创建时间: | 2020/9/2 15:50 |
| 更新时间: | 2022/3/25 23:00 |
| 作者: | Chris |
| 来源: | https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md |
springcloud + springcloud alibaba
是一种架构,将单一应用分成一组小的服务,服务之间通过轻量级的机制相互调用,通常是基于http协议的restful api。
每个服务运行在独立的进程中,并且能够被独立的部署
1. small services
2. lightweigt mechanisms
3. own process 独立进程
4. independency deployable
分步式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶

服务网关 > 服务注册发与发现 > 配置中心
git源码地址
https://github.com/spring-projects/spring-boot/releases
SpringBoot2.0 新特性
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Releases
git源码地址
https://github.com/spring-projects/spring-cloud/wiki
https://projects.spring.io/spring-cloud
https://spring.io/projects/spring-cloud#overview
https://start.spring.io/actuator/info
https://tool.lu/
{
"git": {
"branch": "ee9d426af8dd722b791e1c5f0c612d2bf95a8682",
"commit": {
"id": "ee9d426",
"time": "2020-08-16T20:32:04Z"
}
},
"build": {
"version": "0.0.1-SNAPSHOT",
"artifact": "start-site",
"versions": {
"spring-boot": "2.3.1.RELEASE",
"initializr": "0.9.1-SNAPSHOT"
},
"name": "start.spring.io website",
"time": "2020-08-16T20:33:29.306Z",
"group": "io.spring.start"
},
"bom-ranges": {
"azure": {
"2.0.10": "Spring Boot >=2.0.0.RELEASE and <2.1.0.RELEASE",
"2.1.10": "Spring Boot >=2.1.0.RELEASE and <2.2.0.M1",
"2.2.4": "Spring Boot >=2.2.0.M1 and <2.3.0.M1",
"2.3.1": "Spring Boot >=2.3.0.M1"
},
"codecentric-spring-boot-admin": {
"2.0.6": "Spring Boot >=2.0.0.M1 and <2.1.0.M1",
"2.1.6": "Spring Boot >=2.1.0.M1 and <2.2.0.M1",
"2.2.4": "Spring Boot >=2.2.0.M1 and <2.3.0.M1",
"2.3.0": "Spring Boot >=2.3.0.M1 and <2.4.0-M1"
},
"solace-spring-boot": {
"1.0.0": "Spring Boot >=2.2.0.RELEASE and <2.3.0.M1",
"1.1.0": "Spring Boot >=2.3.0.M1"
},
"solace-spring-cloud": {
"1.0.0": "Spring Boot >=2.2.0.RELEASE and <2.3.0.M1",
"1.1.1": "Spring Boot >=2.3.0.M1"
},
"spring-cloud": {
"Finchley.M2": "Spring Boot >=2.0.0.M3 and <2.0.0.M5",
"Finchley.M3": "Spring Boot >=2.0.0.M5 and <=2.0.0.M5",
"Finchley.M4": "Spring Boot >=2.0.0.M6 and <=2.0.0.M6",
"Finchley.M5": "Spring Boot >=2.0.0.M7 and <=2.0.0.M7",
"Finchley.M6": "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1",
"Finchley.M7": "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2",
"Finchley.M9": "Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE",
"Finchley.RC1": "Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE",
"Finchley.RC2": "Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE",
"Finchley.SR4": "Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT",
"Finchley.BUILD-SNAPSHOT": "Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3",
"Greenwich.M1": "Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE",
"Greenwich.SR6": "Spring Boot >=2.1.0.RELEASE and <2.1.17.BUILD-SNAPSHOT",
"Greenwich.BUILD-SNAPSHOT": "Spring Boot >=2.1.17.BUILD-SNAPSHOT and <2.2.0.M4",
"Hoxton.SR7": "Spring Boot >=2.2.0.M4 and <2.3.4.BUILD-SNAPSHOT",
"Hoxton.BUILD-SNAPSHOT": "Spring Boot >=2.3.4.BUILD-SNAPSHOT and <2.4.0.M1",
"2020.0.0-SNAPSHOT": "Spring Boot >=2.4.0.M1"
},
"spring-cloud-alibaba": {
"2.2.1.RELEASE": "Spring Boot >=2.2.0.RELEASE and <2.3.0.M1"
},
"spring-cloud-services": {
"2.0.3.RELEASE": "Spring Boot >=2.0.0.RELEASE and <2.1.0.RELEASE",
"2.1.7.RELEASE": "Spring Boot >=2.1.0.RELEASE and <2.2.0.RELEASE",
"2.2.3.RELEASE": "Spring Boot >=2.2.0.RELEASE and <2.3.0.M1"
},
"spring-statemachine": {
"2.0.0.M4": "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1",
"2.0.0.M5": "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2",
"2.0.1.RELEASE": "Spring Boot >=2.0.0.RELEASE"
},
"vaadin": {
"10.0.17": "Spring Boot >=2.0.0.M1 and <2.1.0.M1",
"14.3.3": "Spring Boot >=2.1.0.M1 and <2.4.0-M1"
},
"wavefront": {
"2.0.0": "Spring Boot >=2.1.0.RELEASE"
}
},
"dependency-ranges": {
"okta": {
"1.2.1": "Spring Boot >=2.1.2.RELEASE and <2.2.0.M1",
"1.4.0": "Spring Boot >=2.2.0.M1 and <2.4.0-M1"
},
"mybatis": {
"2.0.1": "Spring Boot >=2.0.0.RELEASE and <2.1.0.RELEASE",
"2.1.3": "Spring Boot >=2.1.0.RELEASE and <2.4.0-M1"
},
"geode": {
"1.2.9.RELEASE": "Spring Boot >=2.2.0.M5 and <2.3.0.M1",
"1.3.2.RELEASE": "Spring Boot >=2.3.0.M1 and <2.4.0-M1",
"1.4.0-M1": "Spring Boot >=2.4.0-M1"
},
"camel": {
"2.22.4": "Spring Boot >=2.0.0.M1 and <2.1.0.M1",
"2.25.2": "Spring Boot >=2.1.0.M1 and <2.2.0.M1",
"3.3.0": "Spring Boot >=2.2.0.M1 and <2.3.0.M1",
"3.4.3": "Spring Boot >=2.3.0.M1 and <2.4.0-M1"
},
"open-service-broker": {
"2.1.3.RELEASE": "Spring Boot >=2.0.0.RELEASE and <2.1.0.M1",
"3.0.4.RELEASE": "Spring Boot >=2.1.0.M1 and <2.2.0.M1",
"3.1.1.RELEASE": "Spring Boot >=2.2.0.M1 and <2.4.0-M1"
}
}
}
"Hoxton.BUILD-SNAPSHOT": "Spring Boot >=2.3.4.BUILD-SNAPSHOT and <2.4.0.M1",
https://docs.spring.io/spring-cloud/docs/Hoxton.SR7/reference/html/

服务开发 SpringBoot
服务注册与发现 Eureka [已停止更新] , ZooKeeper, Consul, Alibaba Nacos
服务负载均衡与调用 Ribbon[趋于停止更新],LoadBanance, Feigh[趋于停止更新], OpenFeign
服务熔断 服务降级 Hystrix[趋于停止更新], Resilience4J, Alibaba Sentinel
服务消息队列
配置中心管理 Springcloud config, Alibaba Nacos
服务网关 Zuul [停止更新], Spring Gateway
服务总线 Spring Bus, Alibaba Nacos
服务监控
全链路追踪
自动化构建部署
服务定时任务调度操作
https://docs.spring.io/spring-cloud/docs/Hoxton.SR7/reference/html/#features
https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md
idea > new > project





1.建module: idea > new > module
2.改pom
3.写YML
4.启动类
5.业务类
添加DEV到pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
添加到父工程的pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
idea设置打开自动构建

更新设置
ctrl+alt+shift+/
构选这两项
compiler.automake.allow.when.app.running
actionSystem.assertFocusAccessFromEdt

是Spring提供的用于访问Rest服务的客户模板工具类,提供了多种便捷访问远程http服务的方法
工程目录 下的.idea文件夹
追加下面内容到worksapce.xml中
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
</component>

新建module cloud-api-commons
提取其它module 里面的entities放到cloud-api-commons 里面的 com.chris.springcloud.entities
删除其它module里面的entities
在 中构建共用工程并放到本地仓库中
cd cloud-api-commons
mvn clean install -Dmaven.test.skip=true
在其它module里面加入依赖
<dependency>
<groupId>com.chris.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${common.api.version}</version>
</dependency>
进入项目根目录构建项目
cd cloud2020
mvn clean package -Dmaven.test.skip=true
ctrl+shift+alt+s
选中对应的项目,右击选择Add
选择Spring

选中Spring, 选择
+号
进入之后再选择+号,并选择Other Files
选中需要添加的Application.yml文件
实现服务的注册与发现,管理服务之间依赖关系,调用,负载均衡及容错等
- CS架构
- Eureke Server 作为服务注册功能的服务器,它是服务注册中心
- 系统中的其它服务使用Eureke Client连接到Eureke Server并维持心跳,这样就可以通过Eureke Server来监控系统中各个服务是否运行正常
- Eureke Client通过轮询(round-robin)默认每30秒向Eureke Server发送心跳,如果Eureke Server在多个心跳周期内(默认为90秒) 没有收到某个结点的心跳,会将这个服务节点从服务注册列表中移除.

建module cloud-eureka-server7001
改pom
<!-- 引入最新的eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
建 yml
server:
port: 7001
tomcat:
uri-encoding: UTF-8
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
# false 表示不向注册中心注册自己
register-with-eureka: false
# false 表示自己就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与交互的地址,查询服务和注册服务都需要这依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
主启动
@EnableEurekaServer 激活服务注册中心,用于管理服务的配置,登记和注册
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain {
public static void main(String[] args) {
SpringApplication.run(EurekaMain.class, args);
}
}
启动
访问 http://localhost:7001/

改pom.xml
在具体的服务module的pom.xml里面引入eureka-client
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
改YML
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true
# 单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureak
主启动
@EnableEurekaClient 激活需要注册的服务本身
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain {
public static void main(String[] args) {
SpringApplication.run(PaymentMain.class, args);
}
}
服务注册成功后效果

相互注册,相互监听
建 module cloud-eureka-server7002
改pom.xml
建yml,同理修改cloud-eureka-server7001中的yml
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
# false 表示不向注册中心注册自己
register-with-eureka: false
# false 表示自己就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka Serve交互的地址,查询服务和注册服务都需要这依赖这个地址
# 相互注册
defaultZone: http://eureka7001.com:7001/eureka/
改映射
进入 C:\Windows\System32\drivers\etc
修改hosts 添加如下内容
######## SpringCloud 2020 ###########
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
服务注册成功后效果
http://eureka7001.com:7001/
http://eureka7002.com:7002/

修改服务提供者cloud-provider-payment8001的yml文件
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
修改服务消息者cloud-consumer-order80的yml文件
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
注册成功后的效果

建module cloud-provider-payment8002
改pom.xml
建yml
和 cloud-provider-payment8001的yml一样, 只是端口号需要改为8002
server:
port: 8002
servlet:
context-path: /api
tomcat:
uri-encoding: UTF-8
启动类
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8002.class, args);
}
}
将整个cloud-provider-payment8001的业务代码copy到cloud-provider-payment8002
启动成功后的效果

负载均衡
修改服务消费者cloud-consumer-order80 > PAYMENT_URL> PAYMENT_URL
// 集群版本 CLOUD-PAYMENT-SERVICE是在Eureka中注册的服务名称
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
"org.springframework.web.client.ResourceAccessException: I/O error on GET request for \"http://CLOUD-PAYMENT-SERVICE/api/payment/get/4\": CLOUD-PAYMENT-SERVICE; nested exception is java.net.UnknownHostException: CLOUD-PAYMENT-SERVICE\r\n\tat org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:751)\r\n\tat org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677)\r\n\tat org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:318)\r\n\tat com.springcloud.order.controller.OrderController.getPayment(OrderController.java:41)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\r\n\tat sun.
需要使用 @LoadBalanced 注解赋予RestTemplate负载均衡能力
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
修改服务提供者的yml文件
增加
instance-id: payment8001
prefer-ip-address: true
eureka:
instance:
instance-id: payment8002
prefer-ip-address: true
修改成功后的效果
鼠标放在服务名称上左下脚会有IP显示
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("service:" + service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getInstanceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return this.discoveryClient;
}
修改启动类
增加@EnableDiscoveryClient 激活服务发现
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
测试
request:
http://localhost:8001/api/payment/discovery
response:
{
"services": [
"cloud-payment-service",
"cloud-order-service"
],
"order": 0
}
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
如果服务注册中心在一定时间内没有收到某个微服务的清求会将该服务的实现注销掉
因为网络等原因(延时,卡顿,拥挤)时,微服务与服务注册中心无法通信,但是微服务本身是健康的,这种情况下注销行为就变的非常危险
Eureka通过自我保护来解决这个问题,默认是开启状态
某个时刻微服务不能用了,Eureka不会立即清理,依旧会对该服务的信息进行保护属于CAP里面的AP设计思想
关闭Eureka自我保护
修改服务注册中心的yml配置
eureka:
server:
# 关闭自我保护,保证不可用的服务即时清理
enable-self-preservation: false
# 注销服务的心跳时间,默认为60秒
eviction-interval-timer-in-ms: 2000

修改服务提供者的yml文件
eureka:
instance:
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
https://github.com/Netflix/eureka/wiki
参考 Zookeeper Installment 笔记
建module
cloud-provider-payment8004
改pom
引入zookeeper客户端
<!--SpringBoot 整合 zookeeper client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
建yml
server:
port: 8004
tomcat:
uri-encoding: utf-8
servlet:
context-path: /api
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: master:2181
启动类
@EnableDiscoveryClient //该注册向使用consul或者zookeeper作为注册中心时注册服务
package com.chris.springcloud.payment;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //该注册向使用consul或者zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
zookeeper jar包冲突的问题
<!--SpringBoot 整合 zookeeper client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper 3.6.1 与服务器安装版本匹配一致-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>

使用zkCli.sh查看
cd bin
./zkCli.sh
[zk: 127.0.0.1:2181(CONNECTED) 11] ls /
[services, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 12] ls /services
[cloud-provider-payment]
测试
request:
http://localhost:8004/api/payment/zk
response:
springcloud with zookeeper:8004 9cf857e1-e7e5-4329-bd7e-61ba7d9eb1f0
服务端查看注册信息
cloud-provider-payment 为zookeeper内部的znode节点
1bc5975a-13e6-42a8-8dae-519d0a3139be 为znode的唯一流水号
[zk: 127.0.0.1:2181(CONNECTED) 11] ls /
[services, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 12] ls /services
[cloud-provider-payment]
[zk: 127.0.0.1:2181(CONNECTED) 13] ls /services/cloud-provider-payment
[1bc5975a-13e6-42a8-8dae-519d0a3139be]
[zk: 127.0.0.1:2181(CONNECTED) 14] ls /services/cloud-provider-payment/1bc5975a-13e6-42a8-8dae-519d0a3139be
[]
[zk: 127.0.0.1:2181(CONNECTED) 15] get /services/cloud-provider-payment/1bc5975a-13e6-42a8-8dae-519d0a3139be
{"name":"cloud-provider-payment","id":"1bc5975a-13e6-42a8-8dae-519d0a3139be","address":"DESKTOP-L9SDH81","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1597988679352,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
{
"name": "cloud-provider-payment",
"id": "1bc5975a-13e6-42a8-8dae-519d0a3139be",
"address": "DESKTOP-L9SDH81",
"port": 8004,
"sslPort": null,
"payload": {
"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
"id": "application-1",
"name": "cloud-provider-payment",
"metadata": {}
},
"registrationTimeUTC": 1597988679352,
"serviceType": "DYNAMIC",
"uriSpec": {
"parts": [
{
"value": "scheme",
"variable": true
},
{
"value": "://",
"variable": false
},
{
"value": "address",
"variable": true
},
{
"value": ":",
"variable": false
},
{
"value": "port",
"variable": true
}
]
}
}
Zookeeper临时节点和持久节点
zookeeper在一定时间内(默认为90秒),会将没有收到心跳的服务注销
建module
cloud-consumerzk-order80
改pom
建yml
server:
port: 80
tomcat:
uri-encoding: utf-8
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: master:2181
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderZKMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderZKMain80.class, args);
}
}
配置类
@Configuration
public class ApplicationContextConfig {
@Bean("restTemplate")
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
业务类
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderZKController {
//cloud-provider-payment 为在zookeeper中注册的znode节点名称
private static final String PAYMENT_URL = "http://cloud-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/zk")
public String paymentInfo() {
return restTemplate.getForObject(PAYMENT_URL + "/api/payment/zk", String.class);
}
}
是什么?
https://www.consul.io/intro
用go开发的,开源的分布式服务发现和配置管理系统
提供了微服务系统中的服务治理,配置中心,控制总线等功能,这些功能中的每一个都可以根据需要单独使用,也可以构建全方位的服务风格。
支持HTTP和DNS协议,提供图形化界面,支持Linux,Mac, Windows
能干什么?
服务发现
健康检查
KV存储
多数据中心
可视化Web界面
下载
https://www.consul.io/downloads
https://www.springcloud.cc/spring-cloud-consul.html
https://cloud.spring.io/spring-cloud-consul/reference/html/#spring-cloud-consul-bus
ubuntu安装
https://learn.hashicorp.com/tutorials/consul/get-started-install?in=consul/getting-started
$ curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
$ sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
$ sudo apt-get update && sudo apt-get install consul
consul --version
## 使用开发模式启动
consul agent -dev -node machine -client 0.0.0.0
## 默认端口8500,也可以指定端口号 -http-port 8888
consul agent -dev -node machine -http-port 8888 -ui -client 0.0.0.0
## 输出成员列表,成员有三种状态分别为 "alive", "left", or "failed"
consul members
建modul
cloud-providerconsul-payment8006
改pom
<!--SpringBoot 整合 consul server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
建yml
server:
port: 8006
tomcat:
uri-encoding: UTF-8
servlet:
context-path: /api
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: master
port: 8500
discovery:
service-name: ${spring.application.name}
health-check-path: ${server.servlet.context-path}/actuator/health
#默认10s
health-check-interval: 3s
主启动
@EnableDiscoveryClient 激活服务发现
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class, args);
}
}
业务类
成功注册后的效果
服务名称为 yml中的 service-name: ${spring.application.name}

测试
request:
http://localhost:8006/api/payment/consul
response:
spirngcloud with consul:8006 25c07bdc-7f97-4ef6-9d42-4fdc79c24126
建module
cloud-consumer-consul-order80
改pom.xml
<!--SpringBoot 整合 consul server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
建yml
server:
port: 80
tomcat:
uri-encoding: UTF-8
spring:
application:
name: consul-consumer-order
cloud:
consul:
host: master
port: 8500
discovery:
service-name: ${spring.application.name}
启动类
@EnableDiscoveryClient 激活服务发现
@SpringBootApplication
@EnableDiscoveryClient
public class OrderConsulMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
配置类
package com.chris.springcloud.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
@Bean("restTemplate")
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
业务类
@RestController
@RequestMapping("/customer")
public class OrderConsulController {
private static final String PAYMENT_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/consul")
public String paymentInfo() {
return restTemplate.getForObject(PAYMENT_URL + "api/payment/consul", String.class);
}
}
注册成功后效果

测试
request:
http://localhost/consumer/payment/consul
response:
spirngcloud with consul:8006 eb16d116-7849-4d8e-8782-0e13aa027a3f
Consul实例的运行状况检查默认为“/ health”,
它是Spring Boot Actuator应用程序中有用端点的默认位置。
如果你使用了servlet路径(例如server.servletPath=/foo)则需要更改这些,即使是执行器应用程序
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: master
port: 8500
discovery:
service-name: ${spring.application.name}
health-check-path: ${server.servlet.context-path}/actuator/health
#默认10s
health-check-interval: 3s
| 名称 | 语言 | CAP | Health Check | 对外协议 | 是否集成到SpringCloud |
|---|---|---|---|---|---|
| Eureka | java | AP | 需配置 | 有 | 已集成 |
| ZooKeeper | java | CP | 支持 | 无,只有Linux客户端 | 已集成 |
| Consul | go | CP | 支持 | HTTP、DNS | 已集成 |
CAP:关注的粒度是数据,而不是整体系统设计策略
Consistency 一致性
Availability 可用性
Partition Tolerance 分区容错性
Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡和服务调用。
Ribbon客户端组件提供了一系列完善的配置项如连接超时,重试等
就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动基于某种规则(轮询,随机等)去连接这些机器
Ribbon同进可以使用实现自定义的负载均衡算法
官方资料:
https://github.com/Netflix/ribbon/wiki/Getting-Started
目前Ribbon已进入维护阶段
未来替换方案:
Spring Cloud Starter LoadBalancer
什么是负载均衡
将请求平均的分配到多个服务上,从而达到系统的高可用(HA)
常见的负载均衡组件有,Nginx, LVS, 硬件F5等
Ribbon本地负载均衡和Nginx服务端的负载均衡
Nginx是服务器端的集成式的负载均衡,客户端所有请求都交给Nginx,然后由Nginx实现请求转发,即负载均衡是由服务器实现的。
Ribbon是本地的进程内的负载均衡,在调用服务接口时,Ribbon会在服务注册中心获取服务注册信息,然后缓存在JVM本地,从而在本地实现RPC远程服务调用技术。
Ribbon集成于消费方服务进程中,消费方通过它来获取服务提供方的地址
Ribbon = 负载均衡算法 + RestTemplate
工作时分为两步
第一步先选择EurekaServer,优先选择同一区域内负载较少的server
第二步再根据用户指定的策略,从获取到的服务注册中心的服务列表中选择一个地址
Ribbon 提供了多种策略:
轮询
随机
根据响应时间加权
官网
https://docs.spring.io/spring/docs/current/javadoc-api/
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
postForObject & getForObject
返回对象为JSON
public CommonResult<Payment> create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL + "/api/payment/create", payment, CommonResult.class);
}
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL + "/api/payment/get/" + id, CommonResult.class);
}
postForEntity & getForEntity
包含头信息,响应状态码,响应体
@GetMapping("/payment/create2")
@SuppressWarnings("unchecked")
public CommonResult<Payment> create2(Payment payment) {
ResponseEntity<CommonResult> entity = restTemplate.postForEntity(PAYMENT_URL + "/api/payment/create",
payment, CommonResult.class);
log.info("status:" + entity.getStatusCode() + ",head:" + entity.getHeaders());
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult<>(444, "create2 method operation fail");
}
}
@GetMapping("/payment/get2/{id}")
@SuppressWarnings("unchecked")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/api/payment/get/" + id,
CommonResult.class);
log.info("status:" + entity.getStatusCode() + ",head:" + entity.getHeaders());
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult(444, "get2 mothod opration fail");
}
}
IRule: 根据特定算法从服务列表中选取一个要访问的服务
AbstractLoadBalancerRule
BestAvailableRule
RandomRule --随机
RetryRule
RoundRobinRule --轮询
ZoneAvoidanceRule
AvailabilityFilteringRule --根据响应时间加权重
WeightedResponseTimeRule
替换Ribbon负载均衡
自定义负载均衡算法不能放在@ComponentScan所能扫描到的包里面
https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-ribbon.html

新增 com.springcloud.myrule.MySelfRule
@Configuration
public class MySelfRule {
@Bean
public IRule myRule() {
// 随机负载均衡算法
return new RandomRule();
}
}
在主启动类上添加@RibbonClient
name为在eureka里面注册的服务应用名称
configuration为新增的负载均衡类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain {
public static void main(String[] args) {
SpringApplication.run(OrderMain.class, args);
}
}
算法
Rest接口第几次请求数 % 服务器集群总数量=实际调用服务器位置下标
每次服务重启后,rest接口计数从1开始
原理
JUC(CAS+自旋锁)
注释RestTemplate上的@LoadBalanced
cloud-consumer-order80
@Configuration
public class ApplicationContextConfig {
@Bean
//手写Ribbon负载均衡算法时去掉@LoadBalanced
//@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
自定义LoadBanancerr接口
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
实现自定义LoadBalancer接口
@Component
@Slf4j
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
public int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
}
while (this.atomicInteger.compareAndSet(current, next));
log.info("第几次访问,次数next:" + next);
return next;
}
实现Controller类
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderController {
@Resource
private RestTemplate restTemplate;
@Resource
private LoadBalancer myLB;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (CollectionUtils.isEmpty(instances)) {
return null;
}
ServiceInstance serviceInstance = myLB.instances(instances);
URI uri = serviceInstance.getUri();
log.info("uri:" + uri);
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
}
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
https://github.com/spring-cloud/spring-cloud-openfeign
Feign是一个声明式的webservice客户端,使用Feign使编写webservice客户端更加简单
Feign的使用方法是定义一个服务接口然后在上面添加注解。
Feign可与Eureka和Ribbon组合使用以支持负载均衡
Feign自身集成Ribbon

建module
cloud-consumer-feign-order80
改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.chris.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${common.api.version}</version>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
建yml
server:
port: 80
tomcat:
uri-encoding: utf-8
spring:
application:
name: cloud-order-feign-service
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
主启动
@EnableFeignClients 激活Feign客户端
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
业务类
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") 声明此接口使用feign作为负载均衡和服务调用接口, CLOUD-PAYMENT-SERVICE为服务注册中心中的服务名称
PaymentFeignService 定义了服务提供者的服务接口PaymentController原样拷贝,URL,方法都需要一样,返回值因为是面向客户端所以需要返回CommonResult
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
@RequestMapping("/api/payment")
public interface PaymentFeignService {
@GetMapping("/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderFeignController {
@Resource
public PaymentFeignService paymentFeignService;
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}
测试类
http://localhost/consumer/payment/get/3
超时原因
消费端等待时间小于服务提供者处理时间
openfeign-ribbon默认消费等待时间为1秒
消费端:
http://localhost/consumer/payment/feign/timeout
服务端:
http://localhost:8002/api/payment/feign/timeout
http://localhost:8001/api/payment/feign/timeout

超时设置
配置yml
# 消费端超时控制
ribbon:
ReadTimeout: 5000 #建立连接所用的时间
ConnectTimeout: 5000 #建立连接后服务端读取可用资源所用的时间
Feign提供了日志打印功能,可以通过配置来调整日志级别,从而对请求进行监控来了解HTTP请求的细节,
从低到高分别是:
None: 默认,不显示任何日志
Basic: 仅记录请求方法,URL,响应状态码及执行时间
Headers: 除了Basic中的信息外,还有请求和响应的头信息
Full: 除了Headers中的信息外,还有请求和响应的正文及元数据
配置LoggerBean
package com.chris.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
配置yml
logging:
level:
# feign 以什么级别监控哪个接口
com.chris.springcloud.service.PaymentFeignService: debug
测试
http://localhost/consumer/payment/get/4
2020-08-27 15:51:42.220 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] ---> GET http://CLOUD-PAYMENT-SERVICE/api/payment/get/4 HTTP/1.1
2020-08-27 15:51:42.220 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] ---> END HTTP (0-byte body)
2020-08-27 15:51:42.244 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] <--- HTTP/1.1 200 (24ms)
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] connection: keep-alive
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] content-type: application/json
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] date: Thu, 27 Aug 2020 07:51:42 GMT
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] keep-alive: timeout=60
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] transfer-encoding: chunked
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById]
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] {"code":200,"message":"query payment success.8001","data":{"id":4,"serial":"ethan"}}
2020-08-27 15:51:42.245 DEBUG 469768 --- [p-nio-80-exec-2] c.c.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] <--- END HTTP (84-byte body)
分布式系统中,一个服务有数十个依赖,每个依赖关系在某个时候不可避免地会出现失败
服务相互调用链路会很长,一个出差,整条链路会停用, 甚至服务雪崩。
fallback: 对方系统不可用的情况下,向服务调用方返回一个符合预期的,可处理的备选响应(FallBack)
什么情况下会出现服务降级:
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池信息量打满
服务降级 > 进而熔断 > 恢复调用链路
当服务达到最大访问负载后直接拒绝访问,然后调用服务降级的方法并返回友好提示
熔断机制是一种应对雪崩效应的一种服务链路保护机制
当检测到务调用响应正常后,恢复调用链路
当失败达到阀值时,默认5秒内20次调用失败,就会启用熔断机制
服务熔断有三种状态
打开
请求不再调用当前服务,内部设置了时钟一般为MTTR(平均故障处理时间),当打开时长超过MTTR时钟时长后则进入半开状态
半开
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。
关闭
circuit breaker不会对服务进行熔断
秒杀,高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行QPS
https://www.cnblogs.com/longxiaojiangi/p/9259745.html
一个处理分布式系统延迟和容错的开源库,在分布式系统中许多服务不可避免的会出现调用失败,如网络延时,卡顿,程序出错等。
Hystrix能够保证在一个服务出现错误的情况下,不全导致整体服务失败,避免级联故障,从而提升系统的高可用性。
断路器是一个开关装置,在某个服务单元故障之后,通过断路器的故障监控,向服务调用方返回一个符合预期的,可处理的备选响应(FallBack)
而不是长时间等待或者抛出一个调用方无法处理的异常,这样就可以保证服务调用方的线程不会长时间被占用,从而避免故障在分布式系统中蔓延,及至雪崩。
服务降级
服务熔断
服务限流
接近实时监控
http://github.com/Netflix/Hystrix/wiki/How-To-Use
http://github.com/Netflix/Hystrix/

建module
cloud-provider-hystrix-payment8001
改pom
<!--Eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
建yml
server:
port: 8001
tomcat:
uri-encoding: UTF-8
servlet:
context-path: /api
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
#defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: hystrix-payment8001
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
主启动
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
业务类
@RestController
@Slf4j
@RequestMapping("/payment/hystrix")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/timeout/{id}")
public String paymentInfo_Ok(@PathVariable("id") int id) {
String result = paymentService.paymentInfo_OK(id);
log.info("result:" + result);
return result;
}
}
@Service
@Slf4j
public class PaymentService {
/**
* @param id
* @return 正常访问
*/
public String paymentInfo_OK(int id) {
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_OK, id:" + id + ".";
}
/**
* @param id
* @return 访问超时
*/
public String paymentInfo_TimeOut(int id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
log.error("error happened paymentInfo_TimeOut ", e);
}
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id:" + id + ". waste time" + "seconds:" + 3;
}
}
测试
request
http://localhost:8001/api/payment/hystrix/ok/4
response
thread pool http-nio-8001-exec-3 paymentInfo_OK, id:4.
request
http://localhost:8001/api/payment/hystrix/timeout/4
response
thread pool http-nio-8001-exec-2 paymentInfo_TimeOut, id:4. waste timeseconds:3
Jmeter高并发测试


建module
cloud-consumer-feign-hystrix-order80
改pom
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
建yml
server:
port: 80
tomcat:
uri-encoding: utf-8
spring:
application:
name: cloud-order-feign-hystrix-service
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: feign-hystrix-order80
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
# 消费端超时控制
ribbon:
ReadTimeout: 5000 #建立连接所用的时间
ConnectTimeout: 5000 #建立连接后服务端读取可用资源所用的时间
主启动
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
业务类
/**
* PaymentFeignService 定义了服务提供者的服务接口[PaymentController]原样拷贝,URL,方法都需要一样
*/
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
@RequestMapping("/api/payment/hystrix")
public interface PaymentHystrixService {
@GetMapping("/ok/{id}")
public String paymentInfo_Ok(@PathVariable("id") int id);
@GetMapping("/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") int id);
}
@RestController
@Slf4j
@RequestMapping("/consumer/payment")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/ok/{id}")
public String paymentInfo_Ok(@PathVariable("id") int id) {
return paymentHystrixService.paymentInfo_Ok(id);
}
@GetMapping("/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") int id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
测试
http://localhost/consumer/payment/ok/4
http://localhost/consumer/payment/timeout/4
Jmeter高并发测试
对hystrix-payment8001开启高并发请求
8001同一层级的其它接口服务被困死,因为Tomcat线程池里面的工作线程已经被占用完
80此时调用8001,会出现客户端请求缓慢
对方服务8001超时了,调用者80不能一直卡死等待,必须服务降级
对方服务8001 down机了,调用者80不能一直卡死等待,必须服务降级
对方服务8001 ok,但调用者80自己出故障或自己对服务响应有时间要求,必须服务降级
设置服务自身超时时间峰值,峰值以内可以正常运行
超过峰值要有兜底的方法,作为服务降级的fallback
业务类启动加 @HystrixCommand
com.chris.springcloud.service.PaymentService
/**
* @param id
* @return 访问超时
*/
@HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = {@HystrixProperty(name =
"execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
public String paymentInfo_TimeOut(int id) {
int sleepTime = 5;
try {
TimeUnit.SECONDS.sleep(sleepTime);
} catch (InterruptedException e) {
log.error("error happened paymentInfo_TimeOut ", e);
}
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id:" + id + ". waste time" + "seconds:" + sleepTime;
}
private String timeOutHandler(int id) {
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOutHandler, id:" + id;
}
主启动
添加@EnableCircuitBreaker
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
测试
request:
http://localhost:8001/api/payment/hystrix/timeout/4
response:
thread pool hystrix-PaymentService-1 paymentInfo_TimeOutHandler, system busy or server callapse, id:4
hystrix 使用自己的线程池对服务进行降级 HystrixTimer-1
更改业务类异常类型
/**
* @param id
* @return 访问超时
*/
@HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = {@HystrixProperty(name =
"execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
public String paymentInfo_TimeOut(int id) {
int i = 10 / 0;
/*int sleepTime = 5;
try {
TimeUnit.SECONDS.sleep(sleepTime);
} catch (InterruptedException e) {
log.error("error happened paymentInfo_TimeOut ", e);
}
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id:" + id + ". waste " +
"time" + "seconds:" + sleepTime;
*/
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id:" + id;
}
private String timeOutHandler(int id) {
return "thread pool " + Thread.currentThread().getName() + " paymentInfo_TimeOutHandler, system busy or " +
"server callapse, id:" + id;
}
测试
request:
http://localhost:8001/api/payment/hystrix/timeout/4
response:
thread pool hystrix-PaymentService-1 paymentInfo_TimeOutHandler, system busy or server callapse, id:4
总结
无论是超时异常或运行异常,只要当前服务不可用,就会作服务降级
一般服务降级都是放在消费端
热部署对Java代码的改动是非常敏感的,但是对于@HystrixCommand 内属性的修改建议重启微服务
修yml,允许feign使用hystrix进行服务降级
feign:
hystrix:
enabled: true
主启动
配置@EnableHystrix 激活Hystrix
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
修改业务类
添加@HystrixCommand
@GetMapping("/timeout/{id}")
@HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = {@HystrixProperty(name =
"execution.isolation.thread.timeoutInMilliseconds", value = "1000")})
public String paymentInfo_TimeOut(@PathVariable("id") int id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
@SuppressWarnings("unused")
private String timeOutHandler(int id) {
return "I'm order80, the service provider system busy or server callapse, id:" + id;
}
测试
request:
http://localhost/consumer/payment/timeout/4
response
I'm order80, the service provider system busy or server callapse, id:4
每个业务方法都对应一个fallback方法,导致代码膨胀
服务降级方法和业务代码混合在一起,导致代码混乱
全局服务降级解决两个问题:
1. 代码膨胀
2. 代码混乱
解决致代码膨胀
业务类
com.chris.springcloud.controller.OrderHystrixController
在类上配置默认服务降级的fallback方法
需要支持服务降级的方法上仍需要配置@HystrixCommand()
@RestController
@Slf4j
@RequestMapping("/consumer/payment")
@DefaultProperties(defaultFallback = "paymentGlobalFallBack")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/ok/{id}")
public String paymentInfo_Ok(@PathVariable("id") int id) {
return paymentHystrixService.paymentInfo_Ok(id);
}
@GetMapping("/timeout2/{id}")
@HystrixCommand()
public String paymentInfo_TimeOut2(@PathVariable("id") int id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
@SuppressWarnings("unused")
public String paymentGlobalFallBack() {
return "system is busy , pls try later!";
}
}
测试
request:
http://localhost/consumer/payment/timeout2/4
response:
system is busy , pls try later!
解决代码混乱
实现PaymentHystrixService接口
@Component
public class PaymentFallBackService implements PaymentHystrixService {
@Override
public String paymentInfo_Ok(int id) {
return "PaymentFallBackService fallback-paymentInfo_Ok, the provider collapsed, try it later.";
}
@Override
public String paymentInfo_TimeOut(int id) {
return "PaymentFallBackService fallback-paymentInfo_TimeOut, the provider collLapsed, try it later.";
}
}
配置FeignClient,使用支持服务降级的fallback实现类
@Component
//配置FeignClient,使用支持服务降级的fallback实现类
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallBackService.class, path = "/api/payment/hystrix")
public interface PaymentHystrixService {
@GetMapping("/ok/{id}")
public String paymentInfo_Ok(@PathVariable("id") int id);
@GetMapping("/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") int id);
}
异常处理
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'com.chris.springcloud.service.PaymentHystrixService' method
com.chris.springcloud.service.PaymentHystrixService#paymentInfo_TimeOut(int)
to {GET /api/payment/hystrix/timeout/{id}}: There is already 'paymentFallBackService' bean method
原因:PaymentFallBackService实现了接口PaymentHystrixService,且因为接口有共用map路径**@RequestMapping("/api/payment/hystrix")**,所以spring在实例化Bean时发现一同一URL下有两个相同的方法,不知道调用哪个
@Component
public class PaymentFallBackService implements PaymentHystrixService
解决办法
将/api/payment/hystrix内嵌到每个方法的URL中
使用@FeignClient的path参数
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallBackService.class, path = "/api/payment/hystrix")
测试
停用PaymentHystrixMain8001服务
request:
http://localhost/consumer/payment/ok/4
response:
PaymentFallBackService fallback-paymentInfo_Ok, the provider collapsed, try it later.
SpringCloud框架通过集成的Hystrix实现服务熔断
Hystrix会监控服务间的调用状况,当失败超过设定的阈值时默认为5秒内20次调用失败,就会启动熔断机制
熔断机制注解为@HystrixCommand
How it Works
https://github.com/Netflix/Hystrix/wiki/How-it-Works
martinfowler
https://martinfowler.com/bliki/CircuitBreaker.html

修改业务类
com/chris/springcloud/service/PaymentService.java
/**
* 模拟服务熔断
* 配置中的属性在抽象类HystrixCommandProperties中定义
*/
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallBack", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"), //请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "20000"), //trip circut before retry 的时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")}) //失败率达到多少后启动熔断
public String paymentCircuitBreaker(int id) {
if (id < 0) {
throw new RuntimeException("id 不能为负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t 调用成功,流水号: " + serialNumber;
}
@SuppressWarnings("unused")
private String paymentCircuitBreakerFallBack(int id) {
return "id 不能为负数, id:" + id;
}
com/chris/springcloud/controller/PaymentController.java:39
/**
* 模拟服务熔断
*/
@GetMapping("/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") int id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("paymentCircuitBreaker, result: " + result);
return result;
}
测试
使用Jmeter多次连续访问此连接后
http://localhost:8001/api/payment/hystrix/circuit/-10
再使用postman 调用此连接
http://localhost:8001/api/payment/hystrix/circuit/10
会发现即使是大于0的正数,仍然走的是服务降级后的fallback方法
id 不能为负数, id:10
等Jmeter访问过后,再使用postman 调用此连接
http://localhost:8001/api/payment/hystrix/circuit/10
求得出正确响应
hystrix-PaymentService-10 调用成功,流水号: fdea7e0aead64adbb1fbb5bb046b2b3c
总结
先启用 enabled
时间窗口 sleepWindowInMilliseconds
达到请求数阈值 requestVolumeThreshold
错误百分比阈值 errorThresholdPercentage
Hystrix 会持续记录通过Hystrix 发起的请求信息包括请求数,成功和失败数等并以Hystrix Dashboard 图形化的结果展示给用户
建module
cloud-consumer-hystrix-dashboard9001
改pom
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.chris.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${common.api.version}</version>
</dependency>
<!--hystrix-dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>
建yml
server:
port: 9001
tomcat:
uri-encoding: UTF-8
主启动
com.chris.springcloud.HystirxDashboardMain9001
@SpringBootApplication
@EnableHystrixDashboard
public class HystirxDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystirxDashboardMain9001.class, args);
}
}
测试
http://localhost:9001/hystrix

确保pom中有web和actuator两个依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在启动类里面添加如下方法
com.chris.springcloud.PaymentHystrixMain8001
/**
* 为解决hystrix dashboard
* Unable to connect to Command Metric Stream.
* 此配置是为了服务监控而配置,与服务容错本身无关,SpringClou要·d升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
@SuppressWarnings("unchecked")
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
测试
http://localhost:8001/api/hystrix.stream

使用Jmeter分别对下面这个请求进行并发访问
http://localhost:8001/api/payment/hystrix/circuit/-10
http://localhost:8001/api/payment/hystrix/circuit/10

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
SpringCloud全家桶里面最重要的东西就是网关
SpringCloud Gateway是原zuul1.x的替代产品
提供一种简单有效的方式对API进行路由并提供强大的过滤链功能,如熔断,限流,重试等
SpringCloud Gateway基于Spring WebFlux实现,而Spring WebFlux基层使用高性能的Reactor模式的通信框架Netty
速度是Zuul的1.6倍
传统的web框架, 如struts2, SpringMvc等都是基于Servlet API并在servlet容器上运行的
在Servlet3.1之后出现了异步非阻塞支持,WebFlux是一个典型的异步非阻塞框架,它的核心是基于Reactor的相关API实现的
可以运行在如Netty,Undertow及支持Servlet3.1的容器上。
Route
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
Predicate
参考的是Java8的java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容例如消息头和请求参数,如果请求与断言相匹配则进行路由
Filter
Filter是的是Spring框架中的GateWayFilter的实例,使用过虑器,可以在请求被路由前或路由后对请求进行修改
总结
客户端向SpringCloud GateWay发出请求,然后由GateWay Handler Mapping中找到请求相匹配的路由,将其发送到Gateway Web Handler, 再通过指定的过滤器链将请求发送到实现的服务执行逻辑,然后返回。
过滤前可以进行参数校验,流量监控,日志输出,协议转换等
过滤后可以做响应内容和响应头的修改,流量监控,日志输出等
建module
cloud-gateway-gateway9527
改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringCloud GateWay-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--
eureka client
网关作为一种微服务也在注册到服务注册中心
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
建yml
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
# 路由的ID,没有固定规则但要求唯一,简易配合服务名
- id: payment_8001_routh1
# 匹配提供服务的路由地址
uri: http://localhost:8001
predicates:
// 断言,路径相匹配的进行路由
- Path=/api/payment/get/**
#路由的ID,没有固定规则但要求唯一,简易配合服务名
- id: payment_8001_routh2
#匹配提供服务的路由地址
uri: http://localhost:8001
predicates:
# 断言,路径相匹配的进行路由
- Path=/api/payment/lb/**
eureka:
client:
#表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
#是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: cloud-gateway-9527
prefer-ip-address: true
建配置类
通过RouteLocatorBuilder类创建路由
package com.chris.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig {
/**
* 当访问地址http://localhost:9527/guonei时会自动转发到https//news.baidu.com/guonei
*/
@Bean
public RouteLocator customRouteLocator_guoji(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("guoji_news", r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
/**
* 当访问地址http://localhost:9527/mil 时会自动转发到https//news.baidu.com/mil
*/
@Bean
public RouteLocator customRouteLocator_mil(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("guoji_news", r -> r.path("/mil").uri("http://news.baidu.com/mil")).build();
return routes.build();
}
}
改yml
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从服务注册中心动态创建路由的功能,利用微服务名称进行路由
routes:
# 路由的ID,没有固定规则但要求唯一,简易配合服务名
- id: payment_8001_routh1
# 需要注意的是uri的协议为lb,表示启用gateway的负载均衡功能
# lb://serviceName是spring cloud gateway在微服务中自动创建的负载均衡uri
uri: lb://cloud-payment-service
predicates:
# 断言,路径相匹配的进行路由
- Path=/api/payment/get/**
- id: payment_8001_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/api/payment/lb/**
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories
spring gateway 启动日志
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [After]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Before]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Between]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Cookie]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Header]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Host]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Method]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Path]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Query]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [RemoteAddr]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Weight]
[ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [CloudFoundryRouteService]
配置yml
predicates:
# 断言,路径相匹配的进行路由
- Path=/api/payment/lb/**
- After=2020-09-11T13:40:01.365+08:00[Asia/Shanghai]
- Cookie=username,zzyy #请求要还有cookie并且带有username=zzyy的键值对
- Header=X-Request-Id,\d+ #请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.chris.com
- Method=GET
- Query=id, \d+ #要有参数名id并且值还要啥整数才能路由
测试
curl http://localhost:9527/api/payment/lb --cookie "username=zzyy"
curl http://localhost:9527/api/payment/lb --cookie "username=zzyy" -H "X-Request-Id:5"
curl http://localhost:9527/api/payment/lb --cookie "username=zzyy" -H "X-Request-Id:5,Host:www.chris.com"
curl http://localhost:9527/api/payment/lb?id=15 --cookie "username=zzyy" -H "X-Request-Id:5"
生命周期
1. pro
2. post
种类
1. 单一的GateWayFilter
2. 全局的GlobalFilter
自定义全局过虑器
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("************** come in MyLogGateWayFilter :" + new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (StrUtil.isEmpty(uname)) {
log.info("************** illegal uname :" + new Date());
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 加载过虑器的顺序
* 一般数字或小,优先级越高
* 全局都是0,放在第一位加载
*/
@Override
public int getOrder() {
return 0;
}
}
https://docs.spring.io/spring-cloud-config/docs/2.2.5.RELEASE/reference/html/
微服务意味着将单体应用中的业务拆分一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。
由于每个服务需要配置信息才能运行,所以一套集中的动态的配置管理是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题
Config为微服务提供了集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置

SpringCloud Config 分为服务端和客户端
ConfigServer 称为分布式配置中心,是一独立微服务应用,用来连接配置服务器,并为客户端提供获取配置信息,加密解密等访问接口
配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和查看配置信息内容
客户端就是一个个微服务,通过指定的配置中心来管理应用资源,以及与业务相关的配置内容。并在启动时从配置中心获取和加载配置信息。
集中配置管理
不同的环境不同的配置,分环境部署例如dev/test/prod/beta/release
动态化更新配置,服务不需要重启就可感知到配置的变化并加载新的配置
将配置以rest接口形式暴露
建module
cloud-config-center-3344
改pom
<!--config server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
写yam
server:
port: 3344
tomcat:
uri-encoding: utf-8
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://github.com/ChrisLi716/springcloud-config.git #git仓库名称
search-paths:
- springcloud-config
label: master #读取分支
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: configcenter3344
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
测试
在hosts文件中配置 127.0.0.1 config-3344.com
http://config-3344.com:3344/master/config-dev.yml
结果:
config:
info: master branch,springcloud-config/config-dev.yml version=1
建module
cloud-config-client-3355
改pom
<!--config client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
写yml
application.yml是用户级的资源配置文件
bootstrap.yml是系统级的资源配置文件,优先级更高,更先加载。
SpringCloud会创建一个BootStrap Context作为Spring应用的Application Context的父上下文,初始化时BootStrap Context负责从外部源加载配置信息,这两个上下文共享一个从外部获取的Environment。
BootStrap的优先级更高,默认情况下不会被本地配置覆盖
server:
port: 3355
spring:
application:
name: clound-config-client
cloud:
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #环境名称
uri: http://localhost:3344 #配置中心地址
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: configcenter3344
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
引入actuator图形化监控模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
修改yml暴露监控端点
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
业务类中添加@RefreshScope注解
@RestController
@Slf4j
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfos")
public String getConfigInfo() {
return configInfo;
}
}
修改完github上中的配置信息后发post请求刷新客户端
curl -X POST http://localhost:3355/actuator/refresh
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
读取master分支的dev配置文件
http://config-3344.com:3344/master/config-dev.yml
读取dev分支的config-dev配置文件
http://config-3344.com:3344/dev/config-dev.yml
在微服务架构系统中,通常会使用
轻量级的消息代理来构建一个共用的消息主题,并让系统所有的微服务实现来订阅,由于该主题中产生的消息会被所有的实例消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要其他连接在该主题上的实例都知道的消息。
Bus是对Config的加强,可以实现分布式的自动刷新配置功能
支持两种消息代理,RabbitMQ和Kafka
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus), 当一个服务刷新数据时,它会将这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置

动态刷新全局广播
动态刷新定点通知
消息更新推给configserver配置中心,再由配置中心广播给其它微服务节点

消息更新推给configClient,再由A去传播其它的微服务节点

建module
cloud-config-client-3366
改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--config client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
建YML
bootstrap.yml
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
利用消息总线触发一个客户端/bus/refresh,而刷新所有的客户端的配置
不建议使用这一种
破坏了微服务的责任单一性,因为微服务本向即是业务模块又承担了配置刷新的职责
破坏了微服务各个节点的对等性,例如订单模块有三个集群节点,其中一个有配置刷新功能而其它两个没有配置刷新功能
利用消息总线触发一个服务端/bus/refresh,而刷新所有的客户端的配置
修改配置服务端configServer
cloud-config-center-3344
pom
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
application.yml
#rabbitmq相关配置
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
# 暴露bus刷新配置监控端点
management:
endpoints:
web:
exposure:
include: "bus-refresh"
修改配置客户端configClient
cloud-config-client-3355
cloud-config-client-3366
bootstrap.yml
#rabbitmq相关配置
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
pom
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
测试
http://config-3344.com:3344/master/config-test.yml
http://localhost:3355/configInfos
http://localhost:3366/configInfos
动态刷新全局广播
curl -X POST http://localhost:3344/actuator/bus-refresh
在github中修改了配置信息后,只刷新微服务3355不刷新微服务3366
在github上修改对应的配置文件
发送post请求刷新对应的微服务3355
命令:curl -X POST http://config-3344.com:3344/actuator/bus-refresh/application-name:port
curl -X POST http://config-3344.com:3344/actuator/bus-refresh/cloud-config-client:3355
https://spring.io/projects/spring-cloud-stream#overview
https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/
https://m.wang1314.com/doc/webapp/topic/20971999.html
Spring Cloud Stream是一个构建消息驱动服务的框架
应用程序通过inputs或outputs来与Spring Cloud Stream中的binder对象交互
input 对应于消息者,output 对应于生产者
通过配置来绑定binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互
Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置,引用 了,发布-订阅,消费组,分区三个核心概念
目前Spring Cloud Stream只支持 RabbitMQ 和 Kafka

屏蔽底层消息中间件的差异,降低切换,开发和维护成本,统一消息编程模型
不需要关注具体MQ的实现细节,只需要用一种适配绑定的方式,自动的在各MQ之间进行切换
binder 可以很方便的连接消息中间件
channel 通道,是Queue队列的一种抽象,在消息中间件中是实现消息存储和转发的媒介
Source和Sink可以理解为消息的输出和输入,从Stream生产消息就是输出,消费消息就是输入

middleWare消息中间件,目前只支持rabbitMQ和kafka
@Input 标识输入通道,通过输入通道接收消息进入应用程序
@Output标识输出通道,生产的消息通过输出通道离开应用程序
@StreamListener,监听队列,用于消费者的队列的消息接收
@EnableBinding 指定信道channel和exchange绑定在一起

建module
cloud-stream-rabbitmq-provider8801
改pom
<dependencies>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--stream rabbitmq-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- 一般通用配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies></dependencies>
建yml
server:
port: 8801
tomcat:
uri-encoding: UTF-8
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
bindings: #服务的整合处理
output: #一个通道的名称
destination: studyExchange #要使用的Exchange的名称
content-type: application/json #设置消息类型,文本设置为 text/plain]
binder: defaultRabbit
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: stream-provider8801
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class, args);
}
}
业务类
public interface IMessageProvider {
String send();
}
import cn.hutool.core.lang.UUID;
import com.chris.springcloud.service.IMessageProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import javax.annotation.Resource;
//定义消息的推送信道
@EnableBinding(Source.class)
@Slf4j
public class MessageProviderImpl implements IMessageProvider {
//消息生产信道
@Resource
private MessageChannel output;
@Override
public String send() {
String uuid = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(uuid).build());
log.info("uuid:" + uuid);
return uuid;
}
}
@RestController
public class SendMsgController {
@Resource
private IMessageProvider messageProvider;
@GetMapping("/sendMsg")
public String sendMessage(){
return messageProvider.send();
}
}
测试
http://localhost:8801/sendMsg
建module
cloud-stream-rabbitmq-consumer8802
改pom
<dependencies>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--stream rabbitmq-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
建yml
server:
port: 8802
tomcat:
uri-encoding: UTF-8
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
bindings: #服务的整合处理
input: #一个通道的名称
destination: studyExchange #要使用的Exchange的名称
content-type: application/json #设置消息类型,文本设置为 text/plain]
binder: defaultRabbit
eureka:
client:
# 表示是否将自己注册到eureka-server,默认为true
register-with-eureka: true
# 是否从eureka-server抓取自己的注册信息,默认为true,单节点无所谓,集群必需设置为true以配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
instance:
instance-id: stream-consumer8802
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,默认为30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端收到客户端最后一次心跳后等待的时间上限,超时将注销服务,默认为90秒
lease-expiration-duration-in-seconds: 2
主启动
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class, args);
}
}
业务类
@Component
@EnableBinding(Sink.class)
@Slf4j
public class ConsumMsgListener {
@Value("${server.port}")
private String serverPort;
//监听队列,用于消费者的队列的消息接收
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
log.info("消费者1号,接收到的消息------>" + message.getPayload() + ", port: " + serverPort);
}
}
订单服务做集群部署
如果一个订单被两个服务获取到,就会造成数据错误,甚至重复扣款

分组解决两个问题
消息重复消息问题
处于同一个分组的多个消费者是竞争关系,能够保证消息被其中一个消费者只消费一次。
但不同组是可以重复消费
消息持久化
未分组的微服务在重启后无法正常消费那些未被消费的消息
而有分组的微服务重启后可以正常消费那些未被消费的消息
不同分组
group: Group-Consumer8803
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
bindings: #服务的整合处理
input: #一个通道的名称
destination: studyExchange #要使用的Exchange的名称
content-type: application/json #设置消息类型,文本设置为 text/plain]
binder: defaultRabbit
group: Group-Consumer8803

相同分组
将cloud-stream-rabbitmq-consumer8802和cloud-stream-rabbitmq-consumer8803的分组改为相同名称
group: Group-Consumer-A
消息只会被相同分组中的微服务消费一次
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: master
port: 5672
username: chris
password: 123456
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
bindings: #服务的整合处理
input: #一个通道的名称
destination: studyExchange #要使用的Exchange的名称
content-type: application/json #设置消息类型,文本设置为 text/plain]
binder: defaultRabbit
group: Group-Consumer-A
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,这是形成了一个复杂的分步式调用链路,链路中任何一个节点出现延时或错误都会引起整个请求最后的失败
Sleuth 用于分步式请求链路跟踪并且兼容支持zipkin
https://github.com/spring-cloud/spring-cloud-sleuth
Sleuth 负责请求链路的数据收集
Zipkin 负责数据展示
SpringCloud从F版起就不需要自己构建Zipkin Server了,只需要调用jar包即可
https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/
下载zipkin-server-2.12.9-exec.jar
切换到下载目录下
java -jar zipkin-server-2.12.9-exec.jar
http://127.0.0.1:9411/
一个链路通过一个traceId唯一标识,span标识发起的请求信息,各span通过parentId来关联
span:表示调用链路来源, 通俗的说span就是一次请求信息

整个链路依赖关系如下

向服务提供者8001引入链路跟踪
改pom
<!--引入Sleuth和Zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
改yml
zipkin:
base-url: http://localhost:9411 #链路跟踪数据的展示地址
sleuth:
sampler:
probability: 1 #采样率,介于0到1之间,1表示全部采样,一样用0.5表示一半采样
业务类
com.chris.springcloud.payment.controller.PaymentController
@GetMapping("/zipkin")
public String invokeZipkin() {
return "Hi, test sleuth and zipkin! port:" + serverPort;
}
向服务消费者80引入链路跟踪
改pom
<!--引入Sleuth和Zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
改yml
zipkin:
base-url: http://localhost:9411 #链路跟踪数据的展示地址
sleuth:
sampler:
probability: 1 #采样率,介于0到1之间,1表示全部采样,一样用0.5表一半采样
业务类
com.springcloud.order.controller.OrderController
@GetMapping("/payment/zipkin")
public String testPaymentZipkin() {
return restTemplate.getForObject(PAYMENT_URL + "/api/payment/zipkin", String.class);
}
http://localhost/consumer/payment/zipkin
http://127.0.0.1:9411/zipkin/

https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
https://github.com/alibaba/spring-cloud-alibaba
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
https://spring.io/projects/spring-cloud-alibaba#overview
服务限流和降级:
默认支持Servlet, Feign, RestTemplate,Dubbo和RocketMQ限流降级功能的接入
服务注册与发现:
适配SpringCloud服务注册与发现标准,默认集成了Ribbon的支持。
分布式配置管理:
支持分布式系统中的外部化配置,配置更新时自动刷新。
消息驱动:
基于Stream为微服务应用构建消息驱动能力
阿里云对象存储:
阿里云提供海量,安全,低成本,高可用的云存储服务,支持在任何时间,任何地点,通过任何应用存储和访问任何类型的数据。
分布式任务调度:
提供秒级,精准高可用的的 [基于Cron表达式] 任务调度服务.
分布式事务:
使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
Naming Configuration Services
是服务注册中心和分布式配置中心的组合,相当于Eureak+Config+Bus
替代Eureka作为服务注册中心
替代Config作为分布式配置中心
替代Bus作为服务总线
官网: http://nacos.io
https://nacos.io/zh-cn/index.html
https://github.com/alibaba/Nacos
下载地址:
https://github.com/alibaba/nacos/releases/tag/
下载
nacos-server-1.3.2.tar.gz
配置
cd /opt
tar -zxvf nacos-server-1.3.2.tar.gz
cd nacos/conf/
vi application.poperties
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos?useSSL=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
db.user=root
db.password=65536
新建naocs数据库并在nacos中执行SQL
SQL位置在./conf/nacos-mysql.sql
启动
在nacos的bin目录下执行
./startup.sh -m standalone
查看
http://master:8848/nacos/index.html
用户名和密码为:nacos
建module
cloudalibaba-provider-payment9001
改pom
<!--springcloud alibaba nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
写yml
server:
port: 9001
tomcat:
uri-encoding: UTF-8
servlet:
context-path: /api
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: master:8848 #配置Nacos作为服务注册中心地址
management:
endpoints:
web:
exposure:
include: '*' #监控所有
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}
业务类
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "Nacos registry, server port:" + serverPort + ", id:" + id;
}
}
建module
cloudalibaba-consumer-nacos-order83
改pom
<!--springcloud alibaba nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
写yml
server:
port: 83
tomcat:
uri-encoding: UTF-8
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: master:8848 #配置Nacos地址
#消费者将要去访问的微服务名称(成功注册到nacos的服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider/api
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class, args);
}
}
配置类
nacos-discovery 集成ribbon实现负载均衡和服务调用
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
业务类
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderNacosController {
@Value("${service-url.nacos-user-service}")
private String PAYMENT_SERVICE_NAME;
@Resource
private RestTemplate restTemplate;
@GetMapping("payment/nacos/{id}")
public String getPaymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_SERVICE_NAME + "/payment/nacos/" + id, String.class);
}
}
测试
http://localhost:83/consumer/payment/nacos/45

如果不需要存储服务级别的信息则可以选择AP模式
如果需要在服务级别上编辑或存储配置信息,需要选择CP模式
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switchs?entry=serverModl&value=CP'
| 创建时间: | 2022/4/14 15:01 |
| 更新时间: | 2022/4/14 15:03 |
| 作者: | Chris |
org.springframework.beans.factory.BeanFactoryUtils#beansOfTypeIncludingAncestors(org.springframework.beans.factory.ListableBeanFactory, java.lang.Class<T>, boolean, boolean)
| 创建时间: | 2022/1/4 10:46 |
| 更新时间: | 2022/4/13 22:12 |
| 作者: | Chris |
| 来源: | https://www.php.cn/java-article-416423.html |
static静态代码块,在类加载的时候即自动执行。
构造方法在对象初始化时执行。执行顺序在static静态代码块之后。
PostConstruct注解使用在方法上,这个方法在对象依赖注入初始化之后执行。
CommandLineRunner和ApplicationRunner是SpringBoot所提供的接口,他们都有一个run()方法。所有实现他们的Bean都会在Spring Boot服务启动之后自动地被调用。由于这个特性,它们是一个理想地方去做一些初始化的工作
package com.chris.springboot2022.init;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Author Lilun
* @Date 2022-01-04 10:33
* @Description
**/
@Component
@Order(2)
@Slf4j
public class CommandLineRunnerTest implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("CommandLineRunnerTest is running");
log.info("args:{}", JSONUtil.toJsonStr(args));
log.info("CommandLineRunnerTest end");
}
}
ApplicationRunner与CommandLineRunner做的事情是一样的,也是在服务启动之后其run()方法会被自动地调用,唯一不同的是ApplicationRunner会封装命令行参数,可以很方便地获取到命令行参数和参数值
package com.chris.springboot2022.init;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
/**
* @Author Lilun
* @Date 2022-01-04 10:38
* @Description
**/
@Component
@Slf4j
@Order(1)
public class ApplicationRunnerTest implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("ApplicationRunnerTest is running");
String[] sourceArgs = args.getSourceArgs();
log.info("sourceArgs:{}", JSONUtil.toJsonStr(sourceArgs));
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
List<String> optionValues = args.getOptionValues(optionName);
log.info("optionValues of optionName:{} is {}", optionName, JSONUtil.toJsonStr(optionValues));
}
log.info("ApplicationRunnerTest end");
}
}


无论是
CommandLineRunner还是ApplicationRunner,它们的目的都是在服务启动之后执行一些初始化操作。
如果需要获取命令行参数时则建议使用ApplicationRunner。
另一种场景是我们在服务器上需要执行某个操作,比如修正数据库用户的数据,而又找不到合适的执行入口,那么这就是它们理想的使用场景了。
加载顺序为static>constructer>@PostConstruct>CommandLineRunner和ApplicationRunner.
| 创建时间: | 2022/4/6 22:50 |
| 更新时间: | 2022/4/13 10:44 |
| 作者: | Chris |
| 来源: | https://www.codeleading.com/article/10112032162/ |
- Canal 是用 Java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。
- Canal 主要支持了MySQL的Binlog解析,解析完成后才利用
Canal Client来处理获得的相关数据。- 数据库同步需要阿里的
Otter中间件,基于Canal。
- MySQL的二进制日志可以说 MySQL最重要的日志了,它记录了所有的 DDL和DML(除
了数据查询语句)语句- 以
事件形式记录,还包含语句所执行的消耗的时间,- MySQL的二进制日志是
事务安全型的,即数据和日志的写入是同步完成的。
二进制日志包括两类文件:
二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件,
二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件。
二进制有两个最重要的使用场景:
MySQL Replication在Master端开启Binlog,Master把它的二进制日志传递给 Slaves来达到 Master-Slave 数据一致的目的。
通过使用 MySQL Binlog 工具来使恢复数据。
MySQL Binlog 的格式有三种,分别是
STATEMENT,MIXED,ROW在配置文件中可以选择配置
binlog_format= statement|mixed|row
三种格式的区别:
- binlog 会记录每次一执行写操作的语句。相对 row 模式节省空间
- 但是可能产生不一致性,比如“update tt set create_date=now()” 或者里面有随机函数,如果用 binlog 日志进行恢复,由于执行时间不同可能产生的数据就不同。
- 优点:节省空间。
缺点:有可能造成数据不一致。
- binlog 会记录每次操作后每行记录的变化。
- 优点:保持数据的绝对一致性。因为不管 sql 是什么,引用了什么函数,他只记录
执行后的结果。
缺点:占用较大空间。
- 一定程度上解决了,因为一些情况而造成的 statement
模式下数据不一致问题- 默认还是 statement,在某些情况下譬如:当函数中包含 UUID() 时;包含
AUTO_INCREMENT 字段的表被更新时;执行 INSERT DELAYED 语句时;用 UDF 时;会按照 ROW 的方式进行处理- 优点:节省空间,同时兼顾了一定的一致性。
缺点:还有些极个别情况依旧会造成不一致,另外 statement 和 mixed 对于需要对
binlog 的监控的情况都不方便。综合上面对比,Canal 想做监控分析,选择 row 格式比较合适。
- Master 主库将改变记录,写到二进制日志(Binary Log)中;
- Slave 从库向 MySQL Master 发送 dump 协议,将 Master 主库的 binary log events 拷贝
到它的中继日志(relay log);- Slave 从库读取并重做中继日志中的事件,将改变的数据同步到自己的数据库。

- Canal模拟MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump协议
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 Canal )
- Canal 解析 binary log 对象(原始为 byte 流)

原始场景: 阿里 Otter 中间件的一部分
Otter 是阿里用于进行异地数据库之间的同步框架,Canal 是其中一部分。


抓取业务表的新增变化数据,用于制作实时统计
mysql 配置
配置完之后需要重启mysql
[root@master]# systemctl restart mysqld
[root@master etc]# vi /etc/my.cnf
[mysqld]
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
##如果需要指定多个库可以配置为
binlog-do-db=canal
binlog-do-db=mysql
## 如果要指定所有库包括后面新增的库则可以不配置binlog-do-db
查看binlog文件
[root@master mysql]# cd /var/lib/mysql

写入一条记录后再查看会发现binlog文件大小有变化,说明配置生效了
insert into user_info values ('001', 'chris', 'male');

create user canal identified by '65536';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%' ;
https://github.com/alibaba/canal/releases/tag/canal-1.1.5
[root@master opt]# tar -tvf canal.deployer-1.1.5.tar.gz
[root@master opt]# cd /opt
[root@master opt]# mkdir canal
[root@master opt]# tar -zxvf canal.deployer-1.1.5.tar.gz -C canal/
- 这个文件是 canal 的基本通用配置,canal 端口号默认就是 11111,修改 canal 的
输出 model,默认 tcp,改为输出到 kafka- 多实例配置,一个 canal 服务中可以有多个 instance,conf/下的每一个 example 即是一个实例,每个实例下面都有独立的配置文件。默认只有一个实例 example,如果需要多个实例处理不同的 MySQL 数据的话,直
接拷贝出多个 example,并对其重新命名,命名和配置文件中指定的名称一致,然后修改
canal.properties 中的 canal.destinations=实例 1,实例 2,实例 3。
[root@master]# cd opt/canal/conf
[root@master conf]# vi canal.properties
# tcp, kafka, rocketMQ, rabbitMQ
# tcp是通过服务端和客户端的方式来实现,可能通过代码来获取监控的数据
canal.serverMode = tcp
# 配置需要监控的数据库实例,可以配置多个
canal.destinations = example
[root@master]# /opt/canal/conf/example
[root@master example]# vi instance.properties
# 配置canal实例ID,不要和mysql的master节点ID一样
canal.instance.mysql.slaveId=20
# 配置mysql的master节点地址
canal.instance.master.address=master:3306
# 配置连接 MySQL 的用户名和密码,默认就是我们前面授权的 canal
canal.instance.dbUsername=canal
canal.instance.dbPassword=65536
[root@master bin]# ./startup.sh
[root@master bin]# jps
6659 Jps
6649 CanalLauncher
vi logs/canal/canal.log
vi logs/example/example.log
Message包含多个SQL的执行结果
Entry包含一个SQL的执行结果
StoreValue包含了当前SQL影响的已经序列化的数据
RowChange是StoreValue 反序列化后得到的数据
从RowChange可以获取列名和列值,当前的数据类型EventType和数据本身RowDataList
EventType=update时,数据包含更新前和更新后的数据
EventType=Insert时,只有写入后的数据
EventType=Delete时,只有删除前的数据


package com.chris.canal.tcp.autoinvoke;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author Chris
* @date 2022-04-04 12:12 PM
*/
@Component
@Slf4j
public class CanalClientListener implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
InetSocketAddress socketAddress = new InetSocketAddress("master", 11111);
String canalMysqlInstance = "example";
// 获取 canal 连接对象
CanalConnector canalConnector = CanalConnectors.newSingleConnector(socketAddress, canalMysqlInstance, "", "");
while (true) {
// 获取连接
canalConnector.connect();
// 指定要订阅的数据库和表
canalConnector.subscribe("canal.*");
// 获取 Message, 设置为一次性获取100个SQL的变化数据,超过100条返回100条,没有100则不需要等待立即返回现有的条数
Message message = canalConnector.get(100);
List<CanalEntry.Entry> entries = message.getEntries();
if (CollUtil.isNotEmpty(entries)) {
log.info("get entries");
for (CanalEntry.Entry entry : entries) {
String tableName = entry.getHeader().getTableName();
CanalEntry.EntryType entryType = entry.getEntryType();
if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {
ByteString storeValue = entry.getStoreValue();
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
CanalEntry.EventType eventType = rowChange.getEventType();
List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
for (CanalEntry.RowData rowData : rowDatasList) {
List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
JSONObject beforeData = JSONUtil.createObj();
beforeColumnsList.forEach(row -> beforeData.set(row.getName(), row.getValue()));
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
JSONObject afterData = JSONUtil.createObj();
afterColumnsList.forEach(row -> afterData.set(row.getName(), row.getValue()));
log.info("tableName:{},entryType:{},eventType:{},beforeData:{}, afterData:{}", tableName,
entryType, eventType, beforeData, afterData);
}
}
}
} else {
System.out.println("没有数据,休息一会");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
启动应用
2022-04-04 16:37:29.309 INFO 40204 --- [ restartedMain] c.c.canal.tcp.Canalcloud2022Application : Started Canalcloud2022Application in 8.573 seconds (JVM running for 14.349)
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
没有数据,休息一会
更新表数据
- update
tableName:user_info,entryType:ROWDATA,eventType:UPDATE,beforeData:{"id":"002","name":"John","sex":"male"}, afterData:{"id":"002","name":"John-01","sex":"male"}
tableName:user_info,entryType:ROWDATA,eventType:UPDATE,beforeData:{"id":"001","name":"Chris","sex":"male"}, afterData:{"id":"001","name":"Chris-01","sex":"male"}
- insert
tableName:user_info,entryType:ROWDATA,eventType:INSERT,beforeData:{}, afterData:{"id":"003","name":"Rose","sex":"female"}
tableName:user_info,entryType:ROWDATA,eventType:INSERT,beforeData:{}, afterData:{"id":"004","name":"Petter","sex":"male"}
tableName:user_info,entryType:ROWDATA,eventType:INSERT,beforeData:{}, afterData:{"id":"005","name":"Fiona","sex":"female"}
- delete
tableName:user_info,entryType:ROWDATA,eventType:DELETE,beforeData:{"id":"002","name":"John-01","sex":"male"}, afterData:{}
tableName:user_info,entryType:ROWDATA,eventType:DELETE,beforeData:{"id":"003","name":"Rose","sex":"female"}, afterData:{}
| 参数名 | 参数说明 | 默认值 |
|---|---|---|
| canal.instance.filter.regex | 过滤出需要监控的表 | 无 |
| canal.instance.filter.black.regex | 加入黑名单,不需要监控的表 | 无 |
canal可以通过在instance.properties设置
canal.instance.filter.regex,来忽略不关心的数据变更的parse和sink处理,优化性能,同时减少不必要的存储开销。
canal instance启动时,默认加载
instance.properties的canal.instance.filter.regex参数,之后会根据conf/canal/meta.dat文件filter值更新过滤规则。
当客户端调用CanalConnector.subscribe(String filter)方法时,instance再次用filter参数更新过滤规则。meta.dat文件如下:
{
"clientDatas":[
{
"clientIdentity":{
"clientId":1001,
"destination":"hm-test",
"filter":"xxx.*"
},
"cursor":{
"identity":{
"slaveId":-1,
"sourceAddress":{
"address":"xxxx",
"port":3306
}
},
"postion":{
"gtid":"",
"included":false,
"journalName":"mysql-bin.xxx",
"position":xxx,
"serverId":xxx,
"timestamp":1567062470000
}
}
}
],
"destination":"hm-test"
}
所以当你只关心部分库表更新时,设置了
canal.instance.filter.regex,一定不要在客户端调CanalConnector.subscribe(".\…"),不然等于没设置canal.instance.filter.regex。
如果一定要调用
CanalConnector.subscribe(".\…"),那么可以设置instance.properties的canal.instance.filter.black.regex参数添加黑名单,过滤非关注库表。
| 创建时间: | 2022/4/11 15:30 |
| 更新时间: | 2022/4/11 22:03 |
| 作者: | Chris |
spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。
在spring-retry中,所有配置都是基于简单注释的。
在实际工作中,重处理是一个非常常见的场景,比如:
- 发送消息失败。
- 调用远程服务失败。
- 争抢锁失败。
这些错误可能是因为网络波动造成的,等待过后重处理就能成功。通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码。
然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能。
import com.mail.elegant.service.TestRetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;
@Service
public class TestRetryServiceImpl implements TestRetryService {
@Override
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000,multiplier = 2))
public int test(int code) throws Exception{
System.out.println("test被调用,时间:"+ LocalTime.now());
if (code==0){
throw new Exception("情况不对头!");
}
System.out.println("test被调用,情况对头了!");
return 200;
}
}
POM依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
启用@Retryable
@EnableRetry
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
在方法上添加@Retryable
import com.mail.elegant.service.TestRetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;
@Service
public class TestRetryServiceImpl implements TestRetryService {
@Override
@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 3000, multiplier = 1.5))
public int test(int code) throws Exception{
System.out.println("test被调用,时间:"+ LocalTime.now());
if (code==0){
throw new Exception("情况不对头!");
}
System.out.println("test被调用,情况对头了!");
return 200;
}
}
value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所有异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000,我们设置为3000;
multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为2,则第一次重试为3秒,第二次为6秒,第三次为12秒。
test被调用,时间:21:34:18.402
test被调用,时间:21:34:21.404
test被调用,时间:21:34:27.420
test被调用,时间:21:34:39.432
test被调用,时间:21:35:03.444
当重试耗尽时还是失败,会出现什么情况呢?
当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。
Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
@Recover
public int recover(Exception e, int code){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法
return 400;
}
可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
对于@Recover注解的方法,需要特别注意的是:
- 方法的返回值必须与@Retryable方法一致
- 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
- 该回调方法与重试方法写在同一个实现类里面
由于是基于AOP实现,所以不支持类里自调用方法 , 在注解中配置
recover ="recover"没用
@Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 2000, multiplier = 1), recover = "recover")
如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
方法内不能使用try catch,只能往外抛异常
@Recover 注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。
| 创建时间: | 2022/4/11 14:03 |
| 来源: | https://mp.weixin.qq.com/s/U9iVMJzP8TyhxIl8Gww2Gg |
| 创建时间: | 2022/4/11 14:00 |
| 来源: | https://mp.weixin.qq.com/s/IkJwuqwjW8ZxksZp0ECIOg |
| 创建时间: | 2022/2/20 15:59 |
| 更新时间: | 2022/4/11 9:44 |
| 作者: | Chris |
拦截器是spring中的概念,和过滤器类似,可以对用户的请求进行拦截处理。
但是相对于过滤器而言,拦截器要控制的更加细节
拦截器可以在三个地方执行:
拦截器本质上是AOP,符合横切关注点的功能都可以放在拦截器中来实现,主要的应用场景如下:
- 登录验证,判断用户是否登录
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志,用户IP,访问时间,方法签名等
- 处理cookie,本地化,国际化,主题等
- 性能监控,监控请求处理时长等。
- 过滤器只能在容器初始化时被调用一次,在action的生命周期中,拦截器可以被多次调用
- 过滤器可以对几乎所有的请求起作用,而拦截器只对action请求起作用
- 过滤器不能访问action上下文,堆栈中的对象,而拦截器可以访问
- 过滤器依赖于servlet容器,而拦截器不依赖于servlet容器
- 过滤器是基于函数回调,而拦截器是基于java的反射机制的。
- 过滤器不能获取IOC中的bean,而拦截器可以,这点很重要,在拦截器中注入一个service可以调用业务逻辑
- 拦截器是在DispatcherServlet这个servlet中执行的,因此所有的请求最先进入Filter,最后离开Filter, 其顺序如下:
Filter -> Interceptor.preHandler -> Handler -> Interceptor.postHandle -> Interceptor.afterCompletion -> Filter
- 实现
HandlerInterceptor接口并将实现类注入到spring容器中- 实现
WebMvcConfigurer配置器接口,添加拦截路径和放行路径,并把实现类注入到spring容器中
@RestController
@RequestMapping("/error")
@Slf4j
public class ErrorController {
@GetMapping("/showError")
public String showError() {
log.info("ErrorController.showError executed");
return "the error happended";
}
}
@RestController
@RequestMapping("/hello")
@Slf4j
public class HelloController {
@GetMapping("/sayHello")
public String sayHello() {
log.info("HelloController.sayHello executed");
return "hello My HandlerInterceptor";
}
}
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@GetMapping("/getUserInfo/{userName}")
public String sayHello(@PathVariable("userName") String userName) {
log.info("UserController.getUserInfo executed");
return userName;
}
}
@Slf4j
@Component
public class MyInterceptor implements HandlerInterceptor {
/**
* 前置处理,可以中断请求继续
* true 请求继续
* false 中断请求
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("begin myInterceptor");
return true;
}
/**
* 后置处理,即afterReturning通知
* 在controller执行之后,能过拦截器执行一段代码,此时只是controller很乖完毕,视图还没有渲染
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("controller has executed");
}
/**
* 整个请求结束后执行
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
log.info("response result:{}", JSONUtil.toJsonStr(response));
log.info("request done");
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/hello/*").excludePathPatterns("/error/*");
}
}
http://localhost:8081/hello/sayHello
http://localhost:8081/error/showError

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
MyInterceptor1 myInterceptor1;
@Autowired
MyInterceptor2 myInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor1).addPathPatterns("/hello/*").excludePathPatterns("/error/*");
registry.addInterceptor(myInterceptor2).addPathPatterns("/hello/*", "/user/**").excludePathPatterns("/error/*");
}
}
http://localhost:8081/hello/sayHello

修改MyInterceptor2的preHandle,返回false
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("MyInterceptor2 preHandle: begin myInterceptor");
return false;
}
http://localhost:8081/hello/sayHello


多个拦截器的执行顺序和注册顺序有关,先注册先执行
可以通过Order方法来设置执行的顺序,值越小越先执行
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor1).addPathPatterns("/hello/*").excludePathPatterns("/error/*").order(2);
registry.addInterceptor(myInterceptor2).addPathPatterns("/hello/*", "/user/**").excludePathPatterns("/error" + "/*").order(1);
}

| 创建时间: | 2020/9/2 15:07 |
| 更新时间: | 2022/4/10 13:08 |
| 作者: | Chris |
支持加入源代码的特殊语法元数据。
注解可以保存到class文件中,甚至运行期加载的Class对象中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Configuration {
@Target(ElementType.TYPE)
TYPE 表示可以修饰类,接口,注解和枚举类型
PACKAGE 可以用来修饰包
ANNOTATION_TYPE:可以用来修饰注解类型
METHOD:可以用来修饰方法
FIELD:可以用来修饰属性
CONSTRUCTOR:可以用来修饰构造子
LOCAL_VARIABLE:
@Retention(RetentionPolicy.RUNTIME)
source -> class -> runtime
@Documented
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
String key() default "";
}
| 创建时间: | 2022/1/30 16:14 |
| 更新时间: | 2022/4/10 13:00 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/qq_36666651/article/details/81215221 |
https://blog.csdn.net/xiexingshishu/article/details/121619404
org.springframework.util.ReflectionUtils#makeAccessible(java.lang.reflect.Method)
org.springframework.util.ReflectionUtils#handleReflectionException((Exception ex))
https://vimsky.com/examples/detail/java-method-org.springframework.util.ReflectionUtils.doWithFields.html
父类或接口.class.isAssignableFrom(子类或接口.class)
子类实例 instanceof 父类或接口类型
if (!Linkage.class.isAssignableFrom(linkClazz)) {
throw new SdkException("[" + linkClazz.getName() + "]非link对象!");
}
isAssignableFrom() 方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。
isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类
isAssignableFrom() 方法的调用者和参数都是Class对象,调用者为父类或接口,参数为本身或者其子类。
instanceof关键字两个参数,前一个为类的实例,后一个为其本身或者父类或接口的类型。
@Documented
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
String key() default "";
}
// 判断field是否被AnnotationTest注解,如果有,则获取AnnotationTest注解实例
field.isAnnotationPresent(AnnotationTest.class))
AnnotationTest annotation = field.getAnnotation(AnnotationTest.class);
| 创建时间: | 2022/4/10 12:50 |
| 更新时间: | 2022/4/10 12:52 |
| 作者: | Chris |
package com.chris.reflect;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.chris.reflect.Bean.HumanBeing;
import com.chris.reflect.Bean.Male;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.junit.Test;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Objects;
@Slf4j
public class PropertyDescTest {
@Test
public void buildPropertyDesc() {
HumanBeing male = new Male("Chris", "SZ");
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
PropertyDescriptor[] propertyDescriptors = propertyUtilsBean.getPropertyDescriptors(male);
System.out.println(JSONUtil.toJsonPrettyStr(propertyDescriptors));
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String name = propertyDescriptor.getName();
Method readMethod = propertyDescriptor.getReadMethod();
Method writeMethod = propertyDescriptor.getWriteMethod();
Class<?> propertyType = propertyDescriptor.getPropertyType();
JSONObject obj = JSONUtil.createObj();
obj.set("name", name);
obj.set("readMethod", readMethod);
obj.set("writeMethod", writeMethod);
obj.set("propertyType", propertyType);
String s = JSONUtil.toJsonPrettyStr(obj);
System.out.println(s);
}
}
@Test
public void testCopyValueForSameProperty() {
HumanBeing male = new Male("Chris", "SZ");
Male m = null;
try {
m = copyValueForSameProperty(male, Male.class);
log.info("Male:{}", m);
} catch (Exception e) {
log.error("error happened when copy propeties", e);
}
}
private <T> T copyValueForSameProperty(Object source, Class<T> targetClz) throws Exception {
if (Objects.isNull(source)) {
return null;
}
T target = targetClz.newInstance();
// 获取source的所有属性及方法
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
PropertyDescriptor[] descriptors = propertyUtilsBean.getPropertyDescriptors(source);
for (PropertyDescriptor propItem : descriptors) {
// 获取某一个属性的全部信息
// 过滤setclass/getclass属性
if ("class".equals(propItem.getName())) {
continue;
}
try {
// 通过get方法获取对应属性的值
Method method = propItem.getReadMethod();
Object val = method.invoke(source);
// 如果是空,不做处理
if (null == val) {
continue;
}
// 值复制,调用写方法,设置值
// 获取target的当前属性propItem.getName()的信息
PropertyDescriptor prop = propertyUtilsBean.getPropertyDescriptor(target, propItem.getName());
if (null != prop && prop.getWriteMethod() != null) {
prop.getWriteMethod().invoke(target, val);
}
} catch (Exception e) {
log.error("复制出错prop : " + propItem.getDisplayName() + ", source class: " + source.getClass()
+ ";target type :" + targetClz, e);
}
}
return target;
}
}
| 创建时间: | 2022/4/4 17:15 |
| 更新时间: | 2022/4/4 17:17 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/iris_xuting/article/details/102689000 |
Host X is blocked because of many connection errors; unblock with 'mysqladmin flush-hosts'
mysql> flush hosts;
## 或者
mysql> truncate Performance_chema.host_cache;
| 创建时间: | 2022/4/2 18:00 |
| 更新时间: | 2022/4/2 18:01 |
| 作者: | Chris |
使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
**1、**内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
**2、**在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
**3、**创建内部类对象的时刻并不依赖于外围类对象的创建。
**4、**内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
**5、**内部类提供了更好的封装,除了该外围类,其他类都不能访问。
内部类是个编译时的概念,一旦编译成功后,它就与外围类属于两个完全不同的类(当然他们之间还是有联系的)。
对于一个名为OuterClass的外围类和一个名为InnerClass的内部类,在编译成功后,会出现这样两个class文件:OuterClass.class和OuterClass$InnerClass.class。
在成员内部类中要注意两点,
第一:成员内部类中不能存在任何static的变量和方法;
第二:成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。
@Getter
public class Outer {
private String name = "outer";
private String outer = "I'm outer";
private Inner innerInstance;
// 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时
public Inner getInnerInstance() {
if (null == innerInstance) {
innerInstance = new Inner();
}
return innerInstance;
}
@Getter
class Inner {
private String name = "inner";
public Inner() {
System.out.println("construct inner cls");
System.out.println("outer:" + outer + ", " + name);
// if you wanna visit outer'name
System.out.println("outer:" + outer + ", " + Outer.this.name);
}
}


| 创建时间: | 2022/3/30 17:15 |
| 来源: | https://mp.weixin.qq.com/s/yGLg3kLcqcCBhtBqOE0dyQ |
| 创建时间: | 2022/3/30 14:31 |
| 更新时间: | 2022/3/30 15:06 |
| 来源: | https://mp.weixin.qq.com/s/eOsRHIV9UfCg2E5CBuFzLA |






:表示出现该单词时增加相关性









| 创建时间: | 2020/9/2 14:32 |
| 更新时间: | 2022/3/25 14:51 |
| 作者: | Chris |
| 来源: | https://rocketmq.apache.org/release_notes/release-notes-4.9.0/ |
https://rocketmq.apache.org/docs/quick-start/
https://rocketmq.apache.org/release_notes/release-notes-4.9.0/
下载
rocketmq-all-4.9.0-bin-release.zip
cd /opt
mv /tmp/rocketmq-all-4.9.0-bin-release.zip .
unzip rocketmq-all-4.9.0-bin-release.zip
mv rocketmq-all-4.9.0-bin-release rocketmq
配置broker
vi conf/broker.conf
追加如下内容
#name server服务器地址及端口,可以是多个,分号隔开
namesrvAddr=192.168.80.180:9876
#是否自动创建默认topic,生产需保持关闭
autoCreateTopicEnable=true
#brokerIP1:配置broker所在服务器的ip地址,以便Name Server连接
brokerIP1=192.168.80.180
启动nameserver
[root@master]# nohup sh bin/mqnamesrv &
tail -f /logs/rocketmqlogs/nameserver.log
[root@master bin]# jps
3235 Jps
3207 NamesrvStartup
启动broker
-- 无参数启动
nohup sh bin/mqbroker -n master:9876 &
-- 加载参数启动
nohup sh bin/mqbroker -n master:9876 -c conf/broker.conf
autoCreateTopicEnable=true &
tail -f /logs/rocketmqlogs/broker.log
[root@master rocketmq]# jps
3429 Jps
3207 NamesrvStartup
3386 BrokerStartup
rockertmq启动默认设置内存较大会导致启动失败需要手动修改
vi bin/runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
vi bin/runborker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"
[root@master rocketmq]# export NAMESRV_ADDR=localhost:9876
[root@master rocketmq]# sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
SendResult [sendStatus=SEND_OK, msgId=7F000001109B1B6D35866692827F03E6, offsetMsgId=C0A8657F00002A9F00000000000316F4, messageQueue=MessageQueue [topic=TopicTest, brokerName=master, queueId=1], queueOffset=249]
SendResult [sendStatus=SEND_OK, msgId=7F000001109B1B6D35866692828203E7, offsetMsgId=C0A8657F00002A9F00000000000317BF, messageQueue=MessageQueue [topic=TopicTest, brokerName=master, queueId=2], queueOffset=249]
[root@master rocketmq]# sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
ConsumeMessageThread_8 Receive New Messages: [MessageExt [brokerName=master, queueId=3, storeSize=203, queueOffset=229, sysFlag=0, bornTimestamp=1647784877377, bornHost=/192.168.101.127:40132, storeTimestamp=1647784877378, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F000000000002D5EE, commitLogOffset=185838, bodyCRC=2121214082, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=250, CONSUME_START_TIME=1647786877924, UNIQ_KEY=7F000001109B1B6D3586669281410394, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 32, 57, 49, 54], transaction}]]
ConsumeMessageThread_12 Receive New Messages: [MessageExt [brokerName=master, queueId=3, storeSize=203, queueOffset=228, sysFlag=0, bornTimestamp=1647784877348, bornHost=/192.168.101.127:40132, storeTimestamp=1647784877349, storeHost=/192.168.101.127:10911, msgId=C0A8657F00002A9F000000000002D2C2, commitLogOffset=185026, bodyCRC=2030234779, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=250, CONSUME_START_TIME=1647786877846, UNIQ_KEY=7F000001109B1B6D3586669281240390, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 32, 57, 49, 50], transaction}]]
rocketmq服务关闭
先关broker,再关闭nameserver
关闭broker服务 :sh bin/mqshutdown broker
关闭namesrv服务:sh bin/mqshutdown namesrv
https://github.com/apache/rocketmq-dashboard
下载zip包
unzip rocketmq-externals-master.zip
[root@master rocketmq-externals-master]# cd rocketmq-dashboard-master\src\main\resources
vi application.properties
server:
port: 9877
namesrvAddrs:
- master:9876
[root@master rocketmq-console]# mvn clean package -Dmaven.test.skip=true
[root@master rocketmq-console]# cd target/
nohup java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar > /data/mq/rocketmq/rocket-console.log 2>&1 &
issue
ava.lang.RuntimeException: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to <null> failed
at com.google.common.base.Throwables.propagate(Throwables.java:160)
solution
文件位于rocketmq-externals/rocketmq-console/src/main/resources下面
修改application.properties
rocketmq.config.namesrvAddr=master:9876
rocketmq.config.isVIPChannel=false
http://master:9877
| 创建时间: | 2020/9/8 20:13 |
| 更新时间: | 2022/3/20 20:25 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/rentian1/article/details/93768557 |
https://mirrors.tuna.tsinghua.edu.cn/centos/7.8.2003/isos/x86_64/CentOS-7-x86_64-Everything-2003.iso

去勾选

调整参数

调整时区时间

软件选择

安装目的地

kdump 内核崩溃转储机制,会占用一部分内存,且这部分内存对于其它进程是不用的,测试安装时建议去勾选

打开网络开关


创建root用户










[root@localhost chris]# sudo vi /etc/pam.d/system-auth
#password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
password sufficient pam_unix.so sha512 shadow nullok #try_first_pass use_authtok
#password required pam_deny.so
//修改当前用户密码
passwd
//修改root用户密码
sudo passwd root
配置主机名
vi /etc/hostname
master
vi /etc/sysconfig/network
HOSTNAME=master
配置hosts列表
vi /etc/hosts
192.168.101.127 master
参考VMware NAT配置
cd /etc/sysconfig/network-scripts
cp ifcfg-ens33 ifcfg-ens33.bak
vi ifcfg-ens33
ens33是网卡的名字,这个在每台机器上可能是不同的。
修改内容如下:

ONBOOT="yes"
BOOTPROTO=static
IPADDR=192.168.174.127 #静态IP
GATEWAY=192.168.174.2 #默认网关, 与NAT设置中的网关一致
NETMASK=255.255.255.0 #子网掩码
DNS1=192.168.174.2 #DNS 配置与GATEWAY一致
DNS2=8.8.8.8 #DNS 配置
systemctl restart network
开发者直接在已知的环境中编译好,使用者可以直接下载安装升级卸载
Linux中能够提供这些功能的软件有两种,一个是RPM ,另一个是DPCK,而在centos中使用RPM
red head package manager
将软件提前编译打包,然后在rpm里面存放在用以记录软件依赖关系到的相关数据。
在安装时优先查看这些依赖关系数据,如果系统满足这些依赖关系则安装,否则不安装
安装完成之后会将安装数据记录到自己的数据库中,以便后继升级或卸载等。
只能在指定的系统中使用,所以不同厂商的rpm包,甚至同一厂商不同版本的操作系统的rpm包都不能通用

包安装完成后,rpm包的相关文件一般会放在对应的目录下,比如
rpm包的配置文件一般会放在/etc下
执行文件会放在/usr/bin下
链接库文件会放在/usr/lib下
帮助和说明文档一般会放在/usr/share/man和/usr/share/doc目录下
rpm -ivh dhcp-server-4.3.6-30.el8.x86_64.rpm
-i 安装
-v 显示详细信息
-h 显示安装进度
-qf 查询一个文件归属于哪个已安装的软件包
--force 强制
-U 升级,如果系统中有低版本的就会升级,如果系统中没有安装对应的软件包则安装
-F 有条件的升级,会检测用户指定的软件包是否已安装到linux中
-q 查询指定的软件包是否安装
-ql 查询指定的软件包里面所包含的文件列表
-qi 查询指定的软件包的信息,包括开发商,版本,说明
-qa 查询本机已安装的所有软件包
[root@master chris]# rpm -q dhcp-server
package dhcp-server is not installed
yum yellow dog updater, modified 是一个基于rpm却更胜于rpm的软件包管理工具
可以使用yum安装,升级,卸载软件包,yum会自动解决软件包之间的依赖关系
如果把所有的rpm文件都放在同一个目录下,那这个目录就称为yum下载源
/etc/yum.repos.d/
yum允许第三方厂商开发yum插件,让用户可以方便的扩展yum功能,比如说有的yum插件可以选择最快的yum源
插件存放位置:etc/yum/pluginconf.d/xxx.conf
启用或停用插件:配置文件xxx.conf 中 enable 字段 1:启动,0:不启用
yum运行时会获取yum下载源中的软件信息与文件,暂存放在本机的硬盘中,这个暂存的本机的目录称为yum cache
yum缓存的目录为: /var/cache/yum
cd /etc/yum.repos.d/
mkdir repo.bak
mv *.repo repo.bak
vi server.repo
[base]
name=CentOS-$releasever - Base
# 定义软件包的位置,这里使用清华源
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/os/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
enabled=1 # 1:启动,0:不启用
gpgcheck=1 # 下载软件包时是否检查数字签名
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-7
样例:
[base]
name=CentOS-$releasever - Base
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/os/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-7
#released updates
[updates]
name=CentOS-$releasever - Updates
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/updates/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-7
#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/extras/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-7
#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos/$releasever/centosplus/$basearch/
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-7
~
yum install httpd
-y 如果遇到问题自动回答yes
--installroot=/path 指定软件包安装时的根目录
yum install 'package-group-name' -y 安装一组软件包,建议用引号将组名引起来
yum clean all
有时yum运行不正常,很可能是因为缓存数据导致的,所以需要清理一下缓存
yum makecache fast
yum list 查看yum源中已包含的软件包, 会列出所有的软件包 yum list | more 按空格翻页 按q退出
yum grouplist 以组的形式查看yum源中有哪些软件包组
yum info java-1.8.0-openjdk.x86_64 查看软件包信息
yum search java-1.8.0-openjdk.x86_64 查看某个软件包是否包含在yum源中,等同于 yum list | grep package-name
yum remove package-name 卸载一个软件包
yum groupremove 'package-group-name' 卸载一组软件包
yum list updates 列出所有可更新的软件包
yum list installed 列出所有已安装的软件包
yum list extras 列出所有已安装但不在 Yum Repository 内的软件包
EPEL是一个开源的附加软件包仓库,可用于centos 和 REHL 服务器,它提供了些在默认软件包仓库中不存在的软件包
yum install epel源的下载地址
安装成功后会在 /etc/yum.repos.d/目录下多出来一些epel文件
或者使用如下命令查看系统中的软件源个数
yum repolist
systemV中有一个init命令,可以让service命令去调用/etc/ini.d目录下的服务脚本
/etc/init.d/service-name start/stop/status/restart
service service-name start/stop/status/restart
0 关机
1 单用户
2 无网络多用户
3 字符模式
4 保留
5 图形模式
6 重启
#关闭系统启动3级别中的sshd服务
chkconfig --level 3 sshd off
#forbid the start of firewall when restart system
chkconfig iptables off
chkconfig - updates and queries runlevel information for system services
#查看一个服务在哪个级别启动
chkconfig --list iptables
按功能分类
系统服务:这类服务所服务的对象是系统本身或使用系统的用户
网络服务:这类服务所服务的对象是网络中的其它客户端
按启动方法分类
独立系统服务:
这类服务一旦启动,除非系统关闭或者管理员手动结束,否则会一直在系统后台运行,不管是否会被系统使用到,所以响应快但是占用资源
临时服务:用到的时候启动,使用完后停止,响应速度慢但是节省资源
从centos7开始,systemV和与其对应的init命令被效率更高的systemD及其对应的systemctl命令代替
并且systemctl兼容之前的servicer命令
并且处理所有服务,缩短开机时间
类似于yum,自动解决服务之间的依赖
方便记忆,按照类型对服务分类
兼容init和service命令
/usr/lib/systemd/system/ 服务启动脚本,包含已安装的服务设置文件
/run/systemd/system/ 系统运行过程中的服务脚本,优先级高于上一个文件
/etc/systemd/system/ 管理员手动建立的服务启动脚本,优先级最高
/etc/sysconfig/* 系统功能的默认设置
systemctl -t help

systemctl start sevice-name

status 详解
[root@master chris]# systemctl status atd
atd.service - Job spooling tools
Loaded: loaded (/usr/lib/systemd/system/atd.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2020-09-21 10:19:24 CST; 2min 58s ago
Main PID: 1143 (atd)
Tasks: 1
CGroup: /system.slice/atd.service
1143 /usr/sbin/atd -f
Loaded:加需成功,失败会显示error
Active:服务当前状态
Main PID:服务的主进程号
Tasks:服务的进程个数和线程个数
Memory:当前占用的内存大小
不想让某些服务加入systemctl, mask之后如果使用systemctl 操作就会报错
[root@master chris]# systemctl mask atd
Created symlink from /etc/systemd/system/atd.service to /dev/null.
[root@master chris]# systemctl status atd
● atd.service
Loaded: masked (/dev/null; bad)
Active: active (running) since Mon 2020-09-21 10:19:24 CST; 13min ago
Main PID: 1143 (atd)
CGroup: /system.slice/atd.service
└─1143 /usr/sbin/atd -f
想让某些服务加入systemctl
systemctl unmask atd
systemctl list-units 查看加载到内存中的单元
systemctl list-units --type service | grep mysql 查看mysql服务是否启动
systemctl list-unit-files 查看系统中所有安装的单元文件(存放在/usr/lib/systemd/system/)的启 状态
--type 单元类型
--all 不管状态是什么,列出系统中加载的
systemctl --type service --all
systemctl --type=service --all
#查看一个服务依赖于哪些其它服务
systemctl list-dependencies atd.service
#查看哪些服务依赖于这个服务
systemctl list-dependencies atd.service --reverse
systemctl list-units --type target --all
graphical.target
multi-user.target
shutdown.target
#查看系统默认使用的启动单元
systemctl get-default
#设置系统默认使用的启动单元
systemctl set-default multi-user.target
关机
systemctl poweroff
等同于
init 0
shutdown -h now
重启
systemctl reboot
等同于
init 6
shutdown -r now
挂起
systemctl suspend
休眠
systemctl hibernate

systemctl enable firewalld.service
systemctl restart firewalld.service
firewall-cmd --state 查看状态
firewall-cmd --list-all 查看过滤的列表信息
firewall-cmd --add-service=http --permanent 设置开放的服务
firewall-cmd --add-port=8001 --permanent 设置开放的端口号
firewall-cmd --reload 重新加载防火墙
| 创建时间: | 2020/9/2 13:13 |
| 更新时间: | 2022/3/20 14:30 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/weifeng1463/p/12889300.html |
https://www.jianshu.com/p/2838890f3284
https://www.cnblogs.com/weifeng1463/p/12889300.html
高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性
- 削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)
- 系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)
- 提升性能(当存在一对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)
- 蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)
- 支持事务型消息(消息发送和DB操作保持两方的最终一致性,rabbitmq和 kafka不支持.
- 支持结合rocketmq的多个系统之间数据最终一致性(多方事务,二方事务是前提)
- 支持18个级别的延迟消息(rabbitmq和kafka不支持)
- 支持指定次数和时间间隔的失败消息重发(kafka不支持,rabbitmq需要手动确认)
- 支持consumer端tag过滤,减少不必要的网络传输(rabbitmq和kafka不支持)
- 支持重复消费(rabbitmq不支持,kafka支持)


Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker与Name Server集群中的所有节点建立长连接,定时(每隔30s)注册Topic信息到所有Name Server。Name Server定时(每隔10s)扫描所有存活broker的连接,如果Name Server超过2分钟没有收到心跳,则Name Server断开与Broker的连接。
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Producer每隔30s(由ClientConfig的pollNameServerInterval)从Name server获取所有topic队列的最新情况,这意味着如果Broker不可用,Producer最多30s能够感知,在此期间内发往Broker的所有消息都会失败。
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s中扫描所有存活的连接,如果Broker在2分钟内没有收到心跳数据,则关闭与Producer的连接。
consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
Consumer每隔30s从Name server获取topic的最新队列情况,这意味着Broker不可用时,Consumer最多最需要30s才能感知。
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group的所有Consumer发出通知,Group内的Consumer重新分配队列,然后继续消费。
当Consumer得到master宕机通知后,转向slave消费,slave不能保证master的消息100%都同步过来了,因此会有少量的消息丢失。但是一旦master恢复,未同步过去的消息会被最终消费掉。
消费者队列是消费者连接之后(或者之前有连接过)才创建的。我们将原生的消费者标识由 {IP}@{消费者group}扩展为 {IP}@{消费者group}{topic}{tag},(例如xxx.xxx.xxx.xxx@mqtest_producer-group_2m2sTest_tag-zyk)。
任何一个元素不同,都认为是不同的消费端,每个消费端会拥有一份自己消费队列(默认是broker队列数量*broker数量)。新挂载的消费者对列中拥有commitlog中的所有数据。
一个应用尽可能用一个Topic,消息子类型用tags来标识,tags可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用tags 在broker做消息过滤。
每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过 topic,key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key 尽可能唯一,这样可以避免潜在的哈希冲突。
原理:大事务 = 小事务 + 异步

MQ消息、DB操作一致性方案:
- 发送消息到MQ服务器,此时消息状态为SEND_OK。此消息为consumer不可见。
- 执行DB操作;DB执行成功Commit DB操作,DB执行失败Rollback DB操作。
- 如果DB执行成功,回复MQ服务器,将状态为COMMIT_MESSAGE;如果DB执行失败,回复MQ服务器,将状态改为ROLLBACK_MESSAGE。注意此过程有可能失败。
- MQ内部提供一个名为
“事务状态服务”的服务,此服务会检查事务消息的状态,如果发现消息未COMMIT,则通过Producer启动时注册的TransactionCheckListener来回调业务系统,业务系统在checkLocalTransactionState方法中检查DB事务状态,如果成功,则回复COMMIT_MESSAGE,否则回复ROLLBACK_MESSAGE。
说明:
上面以DB为例,其实此处可以是任何业务或者数据源。
以上SEND_OK、COMMIT_MESSAGE、ROLLBACK_MESSAGE均是client jar提供的状态,在MQ服务器内部是一个数字。
TransactionCheckListener是在消息的commit或者rollback消息丢失的情况下才会回调(上图中灰色部分)。这种消息丢失只存在于断网或者rocketmq集群挂了的情况下。当rocketmq集群挂了,如果采用异步刷盘,存在1s内数据丢失风险,异步刷盘场景下保障事务没有意义。
所以如果要核心业务用Rocketmq解决分布式事务问题,建议选择同步刷盘模式。

当需要保证多方(超过2方)的分布式一致性,上面的两方事务一致性(通过Rocketmq的事务性消息解决)已经无法支持。这个时候需要引入TCC模式思想(
Try-Confirm-Cancel,不清楚的自行百度)。
以上图交易系统为例:
- 交易系统创建订单(往DB插入一条记录),同时发送订单创建消息。通过RocketMq事务性消息保证一致性
- 接着执行完成订单所需的同步核心RPC服务(非核心的系统通过监听MQ消息自行处理,处理结果不会影响交易状态)。执行成功更改订单状态,同时发送MQ消息。
- 交易系统接受自己发送的订单创建消息,通过定时调度系统创建延时回滚任务(或者使用
RocketMq的重试功能,设置第二次发送时间为定时任务的延迟创建时间。在非消息堵塞的情况下,消息第一次到达延迟为1ms左右,这时可能RPC还未执行完,订单状态还未设置为完成,第二次消费时间可以指定)。延迟任务先通过查询订单状态判断订单是否完成,完成则不创建回滚任务,否则创建。
PS:多个RPC可以创建一个回滚任务,通过一个消费组接受一次消息就可以;也可以通过创建多个消费组,一个消息消费多次,每次消费创建一个RPC的回滚任务。 回滚任务失败,通过MQ的重发来重试。
以上是交易系统和其他系统之间保持最终一致性的解决方案。
同步发送是指消息发送方发出数据后,会阻塞直到MQ服务方发回响应消息。
应用场景:此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。

关键代码
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
@Slf4j
public class MqUtil {
private DefaultMQProducer mqProducer;
public void init(DefaultMQProducer mqProducer) {
this.mqProducer = mqProducer;
}
public SendResult send(String topic, String tags, String message) {
try {
Message msg = new Message(topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息到一个Broker
SendResult sendResult = mqProducer.send(msg);
log.info("发送MQ", topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
// 通过sendResult返回消息是否成功送达
return sendResult;
} catch (MQClientException e) {
throw new IpdException(ErrorCode.SYSTEM_ERROR, "MQ主题:" + topic + "不存在,请联系测试人员配置!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 延迟发送
*
* @param topic
* @param tags
* @param message
* @param delayTimeLevel 等级:1:延时1s,2:延时5s,3:延时10s,4:延时30s,5:延时1m
* @return
*/
@SneakyThrows
public SendResult send(String topic, String tags, String message, int delayTimeLevel) {
try {
Message msg = new Message(topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
log.info("延迟发送MQ", topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.setDelayTimeLevel(delayTimeLevel);
// 发送消息到一个Broker
SendResult sendResult = mqProducer.send(msg);
// 通过sendResult返回消息是否成功送达
return sendResult;
} catch (MQClientException e) {
throw new IpdException(ErrorCode.SYSTEM_ERROR, "MQ主题:" + topic + "不存在,请联系测试人员配置!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。MQ 的异步发送,需要用户实现异步发送回调接口(SendCallback),在执行消息的异步发送时,应用不需要等待服务器响应即可直接返回,通过回调接口接收服务器响应, 并对服务器的响应结果进行处理。
应用场景:异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

关键代码:producer.sendAsync(msg, new SendCallback() {//...});
单向(Oneway)发送特点为只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。
此方式发送消息的过程耗时非常短,一般在微秒级别。
应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。

关键代码:producer.sendOneway(msg);
// 定时消息,单位毫秒(ms),在指定时间戳(当前时间之后)进行投递,例如 2016-03-07 16:21:00 投递。
// 如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。
long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2016-03-07 16:21:00").getTime();
msg.setStartDeliverTime(timeStamp);
// 发送消息,只要不抛异常就是成功
SendResult sendResult = producer.send(msg);
Message sendMsg = new Message(topic, tags, message.getBytes());
sendMsg.setDelayTimeLevel(delayLevel);
// 默认3秒超时
SendResult sendResult = rocketMQProducer.send(sendMsg);
| 创建时间: | 2022/3/7 22:42 |
| 更新时间: | 2022/3/19 17:16 |
| 作者: | Chris |
CAS 全称"CompareAndSwap" 即比较并替换
CAS 操作包含三个操作数
- 内存位置 V
- 期望值 A
- 新值 B
如果内存位置与期望值相匹配,那处理器会将该位置的值更新为新值,否则处理顺不做任何处理。
无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS一些特殊情况下返回CAS是否成功,而不提取当前值)
怎么使用JDK中的CAS支持?
java中提供了对CAS的支持,具体在sun.misc.unsafe类中
/**
*var1:表示操作的对象
*var2:表示要操作对象中属性地址的偏移量
*var4:表示要修改数据的期望值
*var5:表示需要修改为的新值
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS通过JNI的代码实现的,JNI [Java Native Interface], 允许java调用其它语言,而compareAndSwapxxx系统的方法就是借助‘C语言’来实现的。
以常用的Intel x86平台来说,最终映射到的cpu指令为
cmpxchg, 这是一条原子指令即不能再拆分,要么都执行要么都不执行,cpu执行此命令时,实现比较并替换的操作!
1 高并发问题
在高并发情况下,多个请求已经拿到了期望值和需要设置的新值,前面的线程进入compareAndSwap方法的线程后会改变原有的期望值为新值,则后面的线程获取锁和cpu资源后进入compareAndSwap会发现内存位置的值 与期望值不匹配,则会退出并进入自悬再次compareAndSwap,直到内存位置的值与期望值匹配修改成功为
2 什么是ABA问题
CAS需要在操作值之前会先比较内存位置的值有没有发生变化,如果没有发生变化则更新。
但是如果一个值原来是A,在CAS方法操作之前被别的线程改成了B,然后又改回成A,那么CAS比较内存位置的值时会发现它的值没有发生变化,但是实际上这个值是有变化的,这就是CAS的ABA问题
public class CAS_ABA {
private static AtomicInteger count = new AtomicInteger(1);
public static void main(String[] args) {
Thread mainThread = new Thread(() -> {
System.out.println("thread name:" + Thread.currentThread().getName() + ", initValue:" + count.get());
int expectValue = count.get();
int newValue = expectValue + 1;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isSucceed = count.compareAndSet(expectValue, newValue);
System.out.println("thread name:" + Thread.currentThread().getName() + ", CAS result:" + isSucceed);
}, "main-thread");
Thread otherThread = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
int incrementValue = count.incrementAndGet();
System.out.println("thread name:" + Thread.currentThread().getName() + "increment value:" + incrementValue);
int decrementValue = count.decrementAndGet();
System.out.println("thread name:" + Thread.currentThread().getName() + "decrement value:" + decrementValue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "other-thread");
mainThread.start();
otherThread.start();
}
}
thread name:main-thread, initValue:1
thread name:other-thread, increment value:2
thread name:other-thread, decrement value:1
thread name:main-thread, CAS result:true
可以看出other-thread线程将count加1后再减1,但mainThread对于count的值变化没有感知,成功修改并返回结果true
如果你的系统对ABA问题不敏感,可以忽略ABA问题
如果你的系统对ABA问题比较敏感,如银行系统,一种方式是不要使用CAS相关的原子类,直接使用
synchronized来保证数据安全性.如果你的系统对ABA问题比较敏感并且对并发也有要求,则可以使用
java.util.concurrent.atomic.AtomicStampedReference
public boolean compareAndSet(V expectedReference, //期望引用
V newReference, //新值的引用
int expectedStamp, //期望引用的版本号
int newStamp){ //新值版本号
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
如果要修改成功,需要expectedReference,expectedStamp与current里面的当前引用及当前引用的版本号相同
public class CAS_ABA2 {
private static final AtomicStampedReference<Integer> count = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
Thread mainThread = new Thread(() -> {
System.out.println("thread name:" + Thread.currentThread().getName() + ", init reference:" + count.getReference() + ", init stamp:" + count.getStamp());
Integer expectReference = count.getReference();
int expectStamp = count.getStamp();
Integer newReference = expectReference + 1;
int newStamp = expectStamp + 1;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isSucceed = count.compareAndSet(expectReference, newReference, expectStamp, newStamp);
System.out.println("thread name:" + Thread.currentThread().getName() + ", CAS result:" + isSucceed + ", " + "reference:" + count.getReference() + ", stamp:" + count.getStamp());
}, "main-thread");
Thread otherThread = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
count.compareAndSet(count.getReference(), count.getReference() + 1, count.getStamp(),
count.getStamp() + 1);
System.out.println("thread name:" + Thread.currentThread().getName() + ", increment reference:" + count.getReference() + ", stamp:" + count.getStamp());
count.compareAndSet(count.getReference(), count.getReference() - 1, count.getStamp(),
count.getStamp() + 1);
System.out.println("thread name:" + Thread.currentThread().getName() + ", decrement reference:" + count.getReference() + ", stamp:" + count.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "other-thread");
mainThread.start();
otherThread.start();
}
}
thread name:main-thread, init reference:1, init stamp:1
thread name:other-thread, increment reference:2, stamp:2
thread name:other-thread, decrement reference:1, stamp:3
thread name:main-thread, CAS result:false, reference:1, stamp:3
| 创建时间: | 2022/3/15 16:37 |
| 更新时间: | 2022/3/15 16:45 |
| 作者: | Chris |
在线上Java程序中经常遇到进程挂掉,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。
Java中得ShutdownHook提供了比较好的方案。
JDK在1.3之后提供了Java Runtime.addShutdownHook(Thread hook) 方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
- 程序正常退出
- 使用System.exit()
- 终端使用Ctrl+C触发的中断
- 系统关闭
- 使用Kill pid命令干掉进程
注:在使用kill -9 pid时,JVM注册的钩子不会被调用。
package com.thread.future.threadpool;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
public class TestShutdownHook {
//简单模拟干活的
static Timer timer = new Timer("job-timer");
//计数干活次数
static AtomicInteger count = new AtomicInteger(0);
/**
* hook线程
*/
static class CleanWorkThread extends Thread {
@Override
public void run() {
System.out.println("clean some work.");
timer.cancel();
try {
Thread.sleep(2 * 1000); //sleep 2s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//将hook线程添加到运行时环境中去
Runtime.getRuntime().addShutdownHook(new CleanWorkThread());
System.out.println("main class start ..... ");
//简单模拟
timer.schedule(new TimerTask() {
@Override
public void run() {
count.getAndIncrement();
//干了10次退出
System.out.println("doing job " + count);
if (count.get() == 10) {
System.exit(0);
}
}
}, 0, 2 * 1000);
}
}
| 创建时间: | 2022/3/14 15:27 |
| 更新时间: | 2022/3/15 16:25 |
| 作者: | Chris |
synchronized关键字可以作用于函数,也可作为函数内的语句,也就是平时说的同步方法和同步块。
如果再细的分类,synchronized可作用于
- instance变量
- object reference(对象引用)
- static函数
- class literals(类名称字面常量)。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或方法当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
1. 每个类实例对应一把锁,每个synchronized方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,
2. 方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
3. 这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized的成员方法中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突.
4. 不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员方法声明为 synchronized ,以控制其对类的静态成员变量的访问。
synchronized Method(){}
- 可以防止多个线程同时访问这个对象的synchronized方法(如果这个对象实例有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象实例中任何一个synchronized方法)。
- 但不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
synchronized static Method(){}
- 防止多个线程同时访问这个类中的
synchronized static方法。- 它可以对类的所有对象实例起作用。
- 使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效
- 非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值
- volatile会禁止指令重排。
可见性:是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。
有序性:即程序执行时按照代码书写的先后顺序执行。
- 在Java内存模型中,允许编译器和处理器对指令进行重排序.
- 重排序过程不会影响到单线程程序的执行,但是却会影响到多线程并发执行的正确性
| - | 可见性 | 有序性 | 原子性 | 线程阻塞 |
|---|---|---|---|---|
| volatile | 1 | 1 | 0 | 0 |
| synchronized | 1 | 1 | 1 | 1 |
java虚拟机有自己的内存模型(Java Memory Model,JMM)
JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见
JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(MainMemory)中,
每个线程都有一个私有的本地内存(LocalMemory),本地内存保存了被该线程使用到的主内存的副本拷贝,
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
这三者之间的交互关系如下
https://www.cnblogs.com/chengxiao/p/6528109.html

| 创建时间: | 2020/9/2 14:48 |
| 更新时间: | 2022/3/14 15:21 |
| 作者: | Chris |
https://www.cnblogs.com/renhui/p/6066852.html
| Java Thread 的使用 |
|---|
| Java Thread 的 run() 与 start() 的区别 |
| Java Thread 的 sleep() 和 wait() 的区别 |
线程从创建到最终的消亡,要经历这几个状态:
1. 创建(new)
2. 就绪(runnable)
3. 运行(running)
4. 阻塞(blocked)
5. time waiting
6. waiting
7. 消亡(dead)
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如 内存资源,程序计数器、Java栈、本地方法栈 都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,
比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、
用户主动让线程等待,
或者被同步块给阻塞,
此时就对应着多个状态:
time waiting(睡眠或等待一定的时间)
waiting(等待被唤醒)
blocked(阻塞)
当由于突然中断或者子任务执行完毕,线程就会被消亡。
下面这副图描述了线程从创建到消亡之间的状态:

在有些教程上将blocked、waiting、time waiting统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和Java中的方法调用联系起来,所以将waiting和time waiting两个状态分离出来。
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。
所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
通过查看java.lang.Thread类的源码可知:

Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如
- name 表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,
- priority 表示线程的优先级(最大值为10,最小值为1,默认值为5),
- daemon 表示线程是否是守护线程,
- target 表示要执行的任务。
下面是Thread类中常用的方法:
以下是关系到线程运行状态的几个方法:
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒 sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
sleep方法不会释放锁
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:
public class Test {
private int i = 10;
private Object object = new Object();
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread1 = test.new MyThread();
MyThread thread2 = test.new MyThread();
thread1.start();
thread2.start();
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:"+i);
}
}
}
}
输出结果:
从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。
注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。
它跟sleep方法类似,同样不会释放锁
但是yield不能控制具体的交出CPU的时间,另外,yield方法只能
让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程
重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
join方法有三个重载版本:
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。
如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。
join方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
看下面一个例子:
public class Test {
public static void main(String[] args) throws IOException {
System.out.println("进入线程"+Thread.currentThread().getName());
Test test = new Test();
MyThread thread1 = test.new MyThread();
thread1.start();
try {
System.out.println("线程"+Thread.currentThread().getName()+"等待");
thread1.join();
System.out.println("线程"+Thread.currentThread().getName()+"继续执行");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("进入线程"+Thread.currentThread().getName());
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");
}
}
}
输出结果:

可以看出,当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。
实际上调用join方法是调用了Object的wait方法,这个可以通过查看源码得知:

wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。
用来
中断一个正处于阻塞状态的线程;调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,
通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
下面看一个例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("进入睡眠状态");
Thread.currentThread().sleep(10000);
System.out.println("睡眠完毕");
} catch (InterruptedException e) {
System.out.println("得到中断异常");
}
System.out.println("run方法执行完毕");
}
}
}
输出结果:

从这里可以看出,通过interrupt方法可以中断处于阻塞状态的线程 那么能不能中断处于非阻塞状态的线程呢?看下面这个例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
运行该程序会发现,while循环会一直运行直到变量i的值超出Integer.MAX_VALUE。所以说:
直接调用interrupt方法不能中断正在运行中的线程。
但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。
比如下面这段代码:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(!isInterrupted() && i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
运行会发现,打印若干个值之后,while循环就停止打印了。
但是一般情况下不建议通过这种方式来中断线程,
一般会在MyThread类中增加一个属性 isStop来标志是否结束while循环,然后再在while循环中判断isStop的值
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop){
i++;
}
}
public void setStop(boolean stop){
this.isStop = stop;
}
}
那么就可以在外面通过调用setStop方法来终止while循环。
7)stop方法
stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。
8)destroy方法
destroy方法也是废弃的方法。基本不会被使用到。
以下是关系到线程属性的几个方法:
1)getId 用来得到线程ID
2)getName和setName 用来得到或者设置线程名称。
3)getPriority和setPriority 用来获取和设置线程优先级。
4)setDaemon和isDaemon 用来设置线程是否成为守护线程和判断线程是否是守护线程
守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:
如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。
而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。
在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:

我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。行格式有4种,分别是Dynamic、Compact、Redundant和Compressed

SHOW VARIABLES LIKE "innodb_default_row_format"
- innodb怎么知道varchar真正有多长?— 变长字段长度列表
- 变长字段数据占用的字节数按照列的顺序逆序存放
- innodb最多分配2个字节去表示这个L,就像unsigned short类型,2个字节,寄存器最多只有16位来让你存这个长度,所以L记录范围是
2^16 - 1 = 65535
这些变长字段(比如varchar)占用的存储空间分为两部分:
CREATE TABLE test (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)) CHARSET = utf8mb4;
INSERT INTO test ( c1, c2, c3, c4 )
VALUES('aaaa', '你好啊', 'cc', 'd'),('eeee', 'fff', NULL, NULL);
select length(c2), char_length(c2) from test;

假设某个字符集中最多需要W字节来表示一个字符
- utf8mb4字符集中的W就是4
- utf8字符集中W就是3
- gbk字符集中的W就是2
- ascii字符集中的W就是1
CREATE TABLE test2 (
c1 VARCHAR(65500) not null,
c2 char(34) not null) CHARSET = ascii;
[42000][1118] Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
CREATE TABLE test2 (
c1 VARCHAR(65500) not null,
c2 char(33) not null) CHARSET = ascii;
CREATE TABLE test3 (
c1 char(65534) not null) CHARSET = ascii;
[42000][1074] Column length too big for column 'c1' (max = 255); use BLOB or TEXT instead
VARCHAR(4),最多存储4个字符,有几个字符存储几个。
存储字节数 = 数据值的字节和 + 1字节(长度标识位)
CHAR(4),最多存储4个字符,不足4个尾部用空格填满。
存储字节数 = 数据值的字节和 + 补位空格数概括地说,
VARCHAR和CHAR都是MySQL的字符串类型,存储多个字符、可设置最大存储的字符数,存储开销都与数据长度、字符集有关
w = 字符集单字符最大字节数
len = VARCHAR的长度标识位
VARCHAR的长度标识位: 存储开销是小于255只要1字节、大于255后使用两字节
n = 列宽度
N = NULL标识列占用字节数
假设一张表中存在N个可空字段,NULL标识位需要[N/8](向上取整)个字节
VARCHAR的宽度=(最大行大小 - N - (n* w + len)) / w
65535 行记录最大字节数
(128x2)为宽度x字符集占用字节数,gbk为两个字节
+2 因为128x2=256,超过255,VARCHAR的长度标识位: 存储开销是小于255只要1字节、大于255后使用两字节
如果把65019全部存储为一个varchar字段,同理VARCHAR的长度标识位需要两个字节即(65019-2)
(65019-2)/2 即为在gbk下的整个varchar字段宽度
# 65535-([(128x2)+2]+[(128x2)+2])=65019
# vm=(65019-2)/2=32508
create table test_varchar_length
(
v1 varchar(128) not null,
v2 varchar(128) not null,
vm varchar(32508) not null
) CHARSET = gbk;
65535 行记录最大字节数
-2即为NULL标识列占用字节数为两个节,8个char(1)+v1 varchar(128),超过8个bit需要用两个字节表示
(1x2x8)为char(1)宽度x字符集占用字节数x8个char(1),gbk为两个字节
(128x2)为宽度x字符集占用字节数,gbk为两个字节
+2因为128x2=256,超过255,VARCHAR的长度标识位: 存储开销是小于255只要1字节、大于255后使用两字节
如果把65019全部存储为一个varchar字段,同理VARCHAR的长度标识位需要两个字节即(65019-2)
(65019-2)/2即为在gbk下的整个varchar字段宽度
# 65535-2-((1x2x8)+[(128x2)+2]+[(128x2)+2])=65001
# vm=(65009-2)/2=32499
create table test_varchar_length2
(
c1 char(1),
c2 char(1),
c3 char(1),
c4 char(1),
c5 char(1),
c6 char(1),
c7 char(1),
c8 char(1),
v1 varchar(128) ,
v2 varchar(128) not null,
vm varchar(32499) not null
) CHARSET = GBK;
| 创建时间: | 2022/1/26 11:26 |
| 更新时间: | 2022/2/21 23:38 |
| 作者: | Chris |
| 来源: | https://dev.mysql.com/doc/refman/8.0/en/create-index.html#create-index-multi-valued |

When the
innodb_stats_persistentsetting is enabled, run theANALYZE TABLEstatement for an InnoDB table after creating an index on that table.
A key_part specification can end with ASC or DESC to specify whether index values are stored in ascending or descending order. The default is ascending if no order specifier is given.
ASC and DESC are not permitted for HASH indexes.
ASC and DESC are also not supported for multi-valued indexes.
As of MySQL 8.0.12, ASC and DESC are not permitted for SPATIAL indexes.
For string columns, indexes can be created that use only the leading part of column values, using
col_name(length)syntax to specify an index prefix length:
Prefixes can be specified for CHAR,VARCHAR, BINARY, and VARBINARY key parts.
Prefixes must be specified for BLOB and TEXT key parts.
If names in the column usually differ in the first 10 characters, lookups performed using this index should not be much slower than using an index created from the entire name column. Also, using column prefixes for indexes can make the index file much smaller, which could save a lot of disk space and might also speed up INSERT operations.
CREATE INDEX part_of_name ON customer (name(10));
the index entry for a given t1 row includes the full col1 value and a prefix of the col2 value consisting of its first 10 characters
CREATE TABLE t1 (
col1 VARCHAR(10),
col2 VARCHAR(20),
INDEX (col1, col2(10))
);
MySQL 8.0.13and higher supports functional key parts
CREATE TABLE t1 (col1 INT, col2 INT, INDEX func_index ((ABS(col1))));
CREATE INDEX idx1 ON t1 ((col1 + col2));
CREATE INDEX idx2 ON t1 ((col1 + col2), (col1 - col2), col1);
ALTER TABLE t1 ADD INDEX ((col1 * 40) DESC);
- In index definitions, enclose expressions within parentheses to distinguish them from columns or column prefixes.
INDEX ((col1 + col2), (col3 - col4))
This produces an error; the expressions are not enclosed within parentheses:
INDEX (col1 + col2, col3 - col4)
- A functional key part cannot consist solely of a column name. For example, this is not permitted:
INDEX ((col1), (col2))
Instead, write the key parts as nonfunctional key parts, without parentheses:
INDEX (col1, col2)
- A functional key part expression cannot refer to column prefixes.
For a workaround, see the discussion of SUBSTRING() and CAST()the
WHEREclause must contain SUBSTRING() with the same arguments.
In the following example, only the second SELECT is able to use the index because that is the only query in which the arguments to SUBSTRING() match the index specification
CREATE TABLE tbl (
col1 LONGTEXT,
INDEX idx1 ((SUBSTRING(col1, 1, 10)))
);
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 9) = '123456789';
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 10) = '1234567890';
- Functional indexes are implemented as hidden virtual generated columns
- The virtual generated column itself requires no storage. The index itself takes up storage space as any other index.
- primary keys cannot include functional key parts, A primary key requires the generated column to be stored, but functional key parts are implemented as virtual generated columns, not stored generated columns.
SPATIALandFULLTEXTindexes cannot have functional key parts.
- If a table contains no primary key, InnoDB automatically promotes the first
UNIQUE NOT NULLindex to the primary key. This is not supported for UNIQUE NOT NULL indexes that have functional key parts.
- To remove a column that is referenced by a functional key part, the index must be removed first. Otherwise, an error occurs.
https://blog.csdn.net/freshlover/article/details/8634610
https://www.cnblogs.com/gengyufei/p/14295405.html
https://blog.csdn.net/bigtree_3721/article/details/87478706
| 创建时间: | 2022/1/30 0:12 |
| 更新时间: | 2022/2/21 21:31 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/sunnycoco05/article/details/78901449 |
package com.chris.springboot2022.bean.proxy;
public class MyTarget {
public boolean printName() {
System.out.println("name:Target-");
return true;
}
}
public interface IHelloService {
void sayHello();
}
@Service
@Data
public class HelloServiceImpl1 implements IHelloService {
private String name = "HelloServiceImpl1";
@Override
public void sayHello() {
System.out.println("hello, this is " + name);
}
}
@Service
@Data
public class HelloServiceImpl2 implements IHelloService {
private String name = "HelloServiceImpl2";
@Override
public void sayHello() {
System.out.println("hello, this is " + name);
}
}
package com.chris.springboot2022.bean.proxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class AroundInteceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println(invocation.getMethod().getName() + " before invoke");
Object proceed = invocation.proceed();
System.out.println(invocation.getMethod().getName() + " after invoke");
return proceed;
}
}
使用spring-aop包中的ProxyFactory创建代理,
并通过addAdvice增加一个环绕方式的拦截
package com.chris.springboot2022;
import com.chris.springboot2022.bean.processor.service.IHelloService;
import com.chris.springboot2022.bean.processor.service.impl.HelloServiceImpl1;
import com.chris.springboot2022.bean.proxy.AroundInteceptor;
import com.chris.springboot2022.bean.proxy.MyTarget;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
public class ProxyFactoryTest {
@Test
public void classProxy() {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new MyTarget());
proxyFactory.addAdvice(new AroundInteceptor());
MyTarget targetProxy = (MyTarget) proxyFactory.getProxy();
targetProxy.printName();
System.out.println(targetProxy.getClass().getName());
}
@Test
public void interfaceProxy() {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setInterfaces(IHelloService.class);
proxyFactory.addAdvice(new AroundInteceptor());
proxyFactory.setTarget(new HelloServiceImpl1());
IHelloService targetProxy = (IHelloService) proxyFactory.getProxy();
targetProxy.sayHello();
System.out.println(targetProxy.getClass().getName());
}
}
printName before invoke
name:Target-
printName after invoke
com.chris.springboot2022.bean.proxy.MyTarget$$EnhancerBySpringCGLIB$$d8c382a4A
sayHello before invoke
hello, this is HelloServiceImpl1
sayHello after invoke
com.sun.proxy.$Proxy9


注释掉后的输出
// proxyFactory.setInterfaces(IHelloService.class);

sayHello before invoke
hello, this is HelloServiceImpl1
sayHello after invoke
com.chris.springboot2022.bean.processor.service.impl.HelloServiceImpl1$$EnhancerBySpringCGLIB$$7b87800d
从运行结果的代理类的class name , 可以看到Spring中的代理对象其实是JDK Proxy和CGLIB Proxy 的结合
- 对于指定接口的代理类使用的是JDK的Proxy
- 对于不指定接口的代理类使用CGLIB的Proxy。
CGLIB是一个强大的、高性能的代码生成库。
其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。
Hibernate作为一个比较受欢迎的ORM框架,同样使用CGLIB来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。
CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib
CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。
我们知道Java中有一个动态代理也是做这个事情的,那我们为什么不直接使用Java动态代理,而要使用CGLIB呢?答案是CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。关于Java动态代理,可以参者这里 Java动态代理分析
| 创建时间: | 2022/2/21 15:25 |
| 更新时间: | 2022/2/21 15:45 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/yaomingyang/article/details/86649072 |
注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,
Bean的加载顺序不受@Order或Ordered接口的影响;
| 创建时间: | 2020/9/8 15:13 |
| 更新时间: | 2022/2/21 10:49 |
| 作者: | Chris |
https://docs.spring.io/spring-boot/docs/2.3.3.RELEASE/reference/htmlsingle/#using-boot-starter
//用来指定这个类为配置类
@Configuration
//在指定条件满足的情况下自动配置类生效
@ConditionalOnXXX
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
//指定自动配置类的顺序
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
//为容器中添加属性
@Bean
//结合xxxProperties类绑定相应的配置
@ConfigurationProperties(prefix = Constants.MYBATIS_PLUS)
//将xxxProperties类注入到容器中使之生效
@EnableConfigurationProperties(MybatisPlusProperties.class)
需要启动就加载的自动配置类,配置在\META-INF\spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
一个空的jar文件,只用来提供辅助性的依赖管理,这些jar文件可能用于自动装配或者其它类库
启动器依赖自动配置
命名:moduleName-spring-boot-starter
这是一个空工程,不需要任何src,resource目录只保留一个pom文件
<groupId>com.chris</groupId>
<artifactId>helloworld-starter</artifactId>
建module
<groupId>com.chris</groupId>
<artifactId>helloworld-starter-autoconfigurer</artifactId>
改pom
<!--所有starter需要引入的基本配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
properties类
package com.chris.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "helloworld.chris")
@Data
public class HelloWorldProperties {
private String prefix;
private String suffix;
}
业务类
package com.chris.service;
import com.chris.config.HelloWorldProperties;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HelloWorldService {
HelloWorldProperties helloWorldProperties;
public String sayHello(String info) {
return helloWorldProperties.getPrefix() + "-" + info + "-" + helloWorldProperties.getSuffix();
}
}
自动配置类
package com.chris.config;
import com.chris.service.HelloWorldService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(HelloWorldProperties.class)
public class HelloworldServiceAutoConfiguration {
@Autowired
HelloWorldProperties helloWorldProperties;
@Bean
@ConditionalOnClass(HelloWorldProperties.class)
public HelloWorldService helloWorldService() {
HelloWorldService helloWorldService = new HelloWorldService();
helloWorldService.setHelloWorldProperties(helloWorldProperties);
return helloWorldService;
}
}
建spring.factories
在resources目录下建META-INF,然后在中建spring.factories
将自动配置类配置到spring.factories文件中
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.chris.config.HelloworldServiceAutoConfiguration
将自动配置模块引入到启动器模块的pom文件中
<!--引入自动配置模块-->
<dependency>
<groupId>com.chris</groupId>
<artifactId>helloworld-starter-autoconfigurer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
构建自动配置模块
cd helloworld-spring-boot-starter-autoconfigurer
mvn clean install
构建启动器模块
cd helloworld-spring-boot-starter
mvn clean install
改pom
<dependency>
<groupId>com.chris</groupId>
<artifactId>helloworld-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
业务类
@RestController
@Slf4j
@RequestMapping("/hello")
public class HelloWorldController {
@Autowired
private HelloWorldService helloWorldService;
@GetMapping("/sayHello")
public String helloworld(@RequestParam("info") String info) {
return helloWorldService.sayHello(info);
}
}
配置yml文件
helloworld:
chris:
prefix: My name is
suffix: Say hello to the world!
测试
request:
http://localhost:4003/api/hello/sayHello?info=Chris
response:
My name is-Chris-Say hello to the world!
| 创建时间: | 2022/2/17 21:21 |
| 更新时间: | 2022/2/17 21:21 |
| 作者: | Chris |
后面就想到了线程池ThreadPoolExecutor,而用的是
SpringBoot项目,可以用Spring提供的对ThreadPoolExecutor封装的线程池ThreadPoolTaskExecutor,直接使用注解启用
在application.yml中添加配置
async:
executor:
thread:
queue_capacity: 99999 # 配置队列大小
name_prefix: async-service- # 配置线程池中的线程的名称前缀
package com.chris.mybatisplus.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Author Lilun
* @Date 2021-06-08 10:45
* @Description
**/
@ConfigurationProperties(prefix = "async.executor.thread")
@Data
public class ThreadPoolConfigBean {
private int corePoolSize;
private int maxPoolSize;
private int queueCapacity;
private String namePrefix;
}
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan("com.chris.mybatisplus.dao.mapper")
@ConfigurationPropertiesScan("com.chris.mybatisplus.config")
public class MybatisPlusMain {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusMain.class, args);
}
}
使用@Configuration和 ==@EnableAsync== 这两个注解,表示这是个配置类,同时启动Spring的异步线程
package com.chris.mybatisplus.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @Author Lilun
* @Date 2021-06-07 20:19
* @Description
**/
@Configuration
@Slf4j
@EnableAsync
//@EnableConfigurationProperties(ThreadPoolConfigBean.class)
public class ExecutorConfig {
@Resource
private ThreadPoolConfigBean configBean;
private static final int corePoolSize = 10 * Runtime.getRuntime().availableProcessors();
private static final int maxPoolSize = corePoolSize * 2;
@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
log.info("start asyncServiceExecutor, corePoolSize:{}, maxPoolSize:{}", corePoolSize, maxPoolSize);
// ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(corePoolSize);
//配置最大线程数
executor.setMaxPoolSize(maxPoolSize);
//配置队列大小
executor.setQueueCapacity(configBean.getQueueCapacity());
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix(configBean.getNamePrefix());
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
}
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan("com.chris.mybatisplus.dao.mapper")
@ConfigurationPropertiesScan("com.chris.mybatisplus.config")
@EnableAsync
public class MybatisPlusMain {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusMain.class, args);
}
}
public interface IAsyncService {
/**
* 执行异步任务
* 可以根据需求,自己加参数拟定,我这里就做个测试演示
*/
void executeAsync();
/**
* 异步调用返回Future
*
* @param i input param
* @return Future
*/
Future<String> asyncInvokeReturnFuture(int i);
}
在executeAsync()方法上增加注解@Async("asyncServiceExecutor")
asyncServiceExecutor方法是前面ExecutorConfig.java中的方法名,表明executeAsync方法进入的线程池是asyncServiceExecutor方法创建的
@Service
@Slf4j
public class AsyncServiceImpl implements IAsyncService {
/**
* 将Service层的服务异步化
* '@Async' 表明executeAsync方法进入的线程池是asyncServiceExecutor方法创建的
*/
@Override
@Async("asyncServiceExecutor")
public void executeAsync() {
log.info("start executeAsync");
log.info("异步线程要做的事情");
log.info("可以在这里执行批量插入等耗时的事情");
log.info("end executeAsync");
}
@SneakyThrows
@Async("asyncServiceExecutor")
public Future<String> asyncInvokeReturnFuture(int i) {
log.info("asyncInvokeReturnFuture, param:{}", i);
Future<String> future = null;
try {
TimeUnit.SECONDS.sleep(5);
future = new AsyncResult<>("invoke success:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
future = new AsyncResult<>("invoke error:" + e.getMessage());
}
return future;
}
}
@RestController
@Slf4j
public class AsyncController {
@Resource
private IAsyncService asyncService;
@GetMapping("async")
public void async() {
asyncService.executeAsync();
}
@GetMapping("asyncInvokeReturnFuture")
public String asyncInvokeReturnFuture(@RequestParam("key") int key) {
String result = "";
try {
result = asyncService.asyncInvokeReturnFuture(key).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return result;
}
}
用postmain或者其他工具来多次测试请求一下
2021-04-16 22:15:47.655 INFO 10516 --- [async-service-5] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:15:47.655 INFO 10516 --- [async-service-5] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:15:47.770 INFO 10516 --- [async-service-1] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:15:47.770 INFO 10516 --- [async-service-1] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:15:47.816 INFO 10516 --- [async-service-2] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:15:47.816 INFO 10516 --- [async-service-2] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:15:48.833 INFO 10516 --- [async-service-3] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:15:48.834 INFO 10516 --- [async-service-3] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:15:48.986 INFO 10516 --- [async-service-4] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:15:48.987 INFO 10516 --- [async-service-4] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
通过以上日志可以发现,[async-service-]是有多个线程的,显然已经在我们配置的线程池中执行了,
并且每次请求中,controller的起始和结束日志都是连续打印的,表明每次请求都快速响应了,
而耗时的操作都留给线程池中的线程去异步执行;
虽然我们已经用上了线程池,但是还不清楚线程池当时的情况,有多少线程在执行,多少在队列中等待呢?
这里我创建了一个ThreadPoolTaskExecutor的子类,在每次提交线程的时候都会将当前线程池的运行状况打印出来
@Slf4j
public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
private void showThreadPoolInfo(String prefix) {
ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();
log.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
this.getThreadNamePrefix(),
prefix,
threadPoolExecutor.getTaskCount(),
threadPoolExecutor.getCompletedTaskCount(),
threadPoolExecutor.getActiveCount(),
threadPoolExecutor.getQueue().size());
}
@Override
public void execute(Runnable task) {
showThreadPoolInfo("1. do execute");
super.execute(task);
}
@Override
public void execute(Runnable task, long startTimeout) {
showThreadPoolInfo("2. do execute");
super.execute(task, startTimeout);
}
@Override
public Future<?> submit(Runnable task) {
showThreadPoolInfo("1. do submit");
return super.submit(task);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
showThreadPoolInfo("2. do submit");
return super.submit(task);
}
@Override
public ListenableFuture<?> submitListenable(Runnable task) {
showThreadPoolInfo("1. do submitListenable");
return super.submitListenable(task);
}
@Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
showThreadPoolInfo("2. do submitListenable");
return super.submitListenable(task);
}
}
2021-04-16 22:23:30.951 INFO 14088 --- [nio-8087-exec-2] u.d.e.e.i.VisiableThreadPoolTaskExecutor : async-service-, 2. do submit,taskCount [0], completedTaskCount [0], activeCount [0], queueSize [0]
2021-04-16 22:23:30.952 INFO 14088 --- [async-service-1] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:23:30.953 INFO 14088 --- [async-service-1] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:23:31.351 INFO 14088 --- [nio-8087-exec-3] u.d.e.e.i.VisiableThreadPoolTaskExecutor : async-service-, 2. do submit,taskCount [1], completedTaskCount [1], activeCount [0], queueSize [0]
2021-04-16 22:23:31.353 INFO 14088 --- [async-service-2] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:23:31.353 INFO 14088 --- [async-service-2] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:23:31.927 INFO 14088 --- [nio-8087-exec-5] u.d.e.e.i.VisiableThreadPoolTaskExecutor : async-service-, 2. do submit,taskCount [2], completedTaskCount [2], activeCount [0], queueSize [0]
2021-04-16 22:23:31.929 INFO 14088 --- [async-service-3] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:23:31.930 INFO 14088 --- [async-service-3] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
2021-04-16 22:23:32.496 INFO 14088 --- [nio-8087-exec-7] u.d.e.e.i.VisiableThreadPoolTaskExecutor : async-service-, 2. do submit,taskCount [3], completedTaskCount [3], activeCount [0], queueSize [0]
2021-04-16 22:23:32.498 INFO 14088 --- [async-service-4] c.u.d.e.executor.impl.AsyncServiceImpl : start executeAsync
异步线程要做的事情
可以在这里执行批量插入等耗时的事情
2021-04-16 22:23:32.499 INFO 14088 --- [async-service-4] c.u.d.e.executor.impl.AsyncServiceImpl : end executeAsync
注意这一行日志:
2021-04-16 22:23:32.496 INFO 14088 --- [nio-8087-exec-7] u.d.e.e.i.VisiableThreadPoolTaskExecutor : async-service-, 2. do submit,taskCount [3], completedTaskCount [3], activeCount [0], queueSize [0]
自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。
在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认线程池最终产生的线程池有且只能设置一个(不能设置多个类继承AsyncConfigurer)
org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setConfigurers
/**
* Collect any {@link AsyncConfigurer} beans through autowiring.
*/
@Autowired
void setConfigurers(ObjectProvider<AsyncConfigurer> configurers) {
Supplier<AsyncConfigurer> configurer = SingletonSupplier.of(() -> {
List<AsyncConfigurer> candidates = configurers.stream().collect(Collectors.toList());
if (CollectionUtils.isEmpty(candidates)) {
return null;
}
if (candidates.size() > 1) {
throw new IllegalStateException("Only one AsyncConfigurer may exist");
}
return candidates.get(0);
});
this.executor = adapt(configurer, AsyncConfigurer::getAsyncExecutor);
this.exceptionHandler = adapt(configurer, AsyncConfigurer::getAsyncUncaughtExceptionHandler);
}
自定义线程池有如下模式:
无论是继承或者重新实现接口,都需指定一个线程池。且重新实现 public Executor getAsyncExecutor()方法。
由于AsyncConfigurer的默认线程池在源码中为空,Spring通过beanFactory.getBean(TaskExecutor.class)先查看是否有线程池,未配置时,通过beanFactory.getBean(==DEFAULT_TASK_EXECUTOR_BEAN_NAME==, Executor.class),
又查询是否存在默认名称为TaskExecutor的线程池。所以可在项目中,定义名称为TaskExecutor的bean生成一个默认线程池。也可不指定线程池的名称,申明一个线程池,本身底层是基于==TaskExecutor.class==便可。
| 创建时间: | 2022/2/17 17:40 |
| 作者: | Chris |
| 创建时间: | 2022/1/6 17:33 |
| 更新时间: | 2022/2/17 15:40 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/itplay/p/10982072.html |
spring的事件监听有三个部分组成,
事件(ApplicationEvent)
监听器 (ApplicationListener)
事件发布操作 (ApplicationContext.publishEvent)
如果在上下文中部署一个实现了
ApplicationListener接口的bean, 每当在一个ApplicationEvent发布到ApplicationContext时,这个bean得到通知。其实这就是标准的Oberver设计模式。
继承ApplicationListener抽象类
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.springframework.context.ApplicationEvent;
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
public class EmailEvent extends ApplicationEvent {
private String address;
private String text;
public EmailEvent(Object source) {
super(source);
}
public EmailEvent(Object source, String address, String text) {
super(source);
this.address = address;
this.text = text;
}
public void print() {
System.out.println("hello Spring ApplicationEvent!");
}
}
实现ApplicationListener接口并覆写onApplicationEvent
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class EmailListener implements ApplicationListener<EmailEvent> {
@Override
public void onApplicationEvent(EmailEvent emailEvent) {
emailEvent.print();
System.out.println("the source is :" + emailEvent.getSource());
System.out.println("the address is :" + emailEvent.getAddress());
System.out.println("the text is :" + emailEvent.getText());
}
}
@SpringBootTest
class Springboot2022ApplicationTests {
@Autowired
private ApplicationContext applicationContext;
@Test
void listenerTest() {
EmailEvent emailEvent = new EmailEvent("hello", "chris@163.com", "this's a email text.");
applicationContext.publishEvent(emailEvent);
}
}
hello Spring ApplicationEvent!
the source is :hello
the address is :chris@163.com
the text is :this's a email text.
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class BlogModifyEvent {
private String text;
private boolean isChanged;
public void print() {
System.out.println("hello Spring BlogModifyEvent!");
}
}
import com.chris.springboot2022.listener.BlogModifyEvent;
public interface IListenerService {
void modifyBlog(BlogModifyEvent blogModifyEvent);
}
import cn.hutool.json.JSONUtil;
import com.chris.springboot2022.listener.BlogModifyEvent;
import com.chris.springboot2022.service.IListenerService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class ListernerServiceImpl implements IListenerService {
@Override
@EventListener
public void modifyBlog(BlogModifyEvent blogModifyEvent) {
blogModifyEvent.print();
log.info("blogModifyEvent:{}", JSONUtil.toJsonStr(blogModifyEvent));
}
}
Spring会为事件创建一个ApplicationListener实例,并从方法参数中获取事件的类型。
一个类中被事件注释的方法数量没有限制,所有相关的事件句柄都会分组到一个类中。
@SpringBootTest
class Springboot2022ApplicationTests {
@Autowired
private ApplicationContext applicationContext;
@Test
void listener2Test() {
BlogModifyEvent blogModifyEvent = new BlogModifyEvent("this's my first blog.", false);
applicationContext.publishEvent(blogModifyEvent);
}
}
hello Spring BlogModifyEvent!
2022-01-07 11:35:25.041 INFO 48388 --- [ main] c.c.s.service.impl.ListernerServiceImpl : blogModifyEvent:{"isChanged":false,"text":"this's my first blog."}
@Override
@EventListener(condition = "#blogModifyEvent.isChanged")
public void modifyBlogWhenChanged(BlogModifyEvent blogModifyEvent) {
System.out.println("blog has been changed!");
log.info("blogModifyEvent:{}", JSONUtil.toJsonStr(blogModifyEvent));
}
@SpringBootTest
class Springboot2022ApplicationTests {
@Autowired
private ApplicationContext applicationContext;
@Test
void listener2Test() {
BlogModifyEvent blogModifyEvent = new BlogModifyEvent("this's my first blog.", true);
applicationContext.publishEvent(blogModifyEvent);
}
}
blog has been changed!
2022-01-07 11:39:36.153 INFO 47168 --- [ main] c.c.s.service.impl.ListernerServiceImpl : blogModifyEvent:{"isChanged":true,"text":"this's my first blog."}
hello Spring BlogModifyEvent!
2022-01-07 11:39:36.154 INFO 47168 --- [ main] c.c.s.service.impl.ListernerServiceImpl : blogModifyEvent:{"isChanged":true,"text":"this's my first blog."}
因为我们绑定了两个监听,所以以isChanged=true时会打印两个监听日志
注释
@EventListener还可以与注释@Async进行组合使用,以提供异步事件处理的机制。
下面的代码中,指定的事件监听器既不会阻塞主要的代码执行,又不会被其它的监听器处理。

| 创建时间: | 2022/2/1 15:47 |
| 更新时间: | 2022/2/1 15:47 |
| 作者: | Chris |
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
| 创建时间: | 2022/1/30 18:21 |
| 更新时间: | 2022/2/1 15:40 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/geekjoker/article/details/79868945 |
作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑

可以定义一个或多个
BeanPostProcessor接口实现类,然后注册到Spring IoC容器中.
多个后置处理器也可以通过实现Ordered接口getOrder方法来定义执行顺序
实现Ordered接口getOrder方法,该方法返回一整数,默认值为 0,优先级最高,值越大优先级越低
ApplicationContext会自动检测在配置文件中实现了BeanPostProcessor接口的所有bean,并把它们注册为后置处理器,然后在容器创建bean的适当时候调用它,因此部署一个后置处理器同部署其他的bean并没有什么区别。
package com.chris.springboot2022.bean.processor;
import com.chris.springboot2022.bean.processor.annotation.RountingInjected;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Map;
@Component
@Slf4j
public class HelloServiceInjectProcessor implements BeanPostProcessor {
@Autowired
private ApplicationContext applicationContext;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
log.info("begin postProcessBeforeInitialization, bean:{}, name:{}", bean.toString(), beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("begin postProcessAfterInitialization, bean:{}, name:{}", bean.toString(), beanName);
Class<?> clz = bean.getClass();
Field[] declaredFields = clz.getDeclaredFields();
for (Field field : declaredFields) {
if (field.isAnnotationPresent(RountingInjected.class)) {
if (!field.getType().isInterface()) {
throw new BeanCreationException("RountingInjected field:{} must bea declared as an interface" +
"@Class:{}", field.getName(), clz.getSimpleName());
}
try {
handleRoutingInjected(field, bean, field.getType());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
private void handleRoutingInjected(Field field, Object bean, Class<?> type) throws IllegalAccessException {
Map<String, ?> candidates = applicationContext.getBeansOfType(type);
field.setAccessible(true);
if (candidates.size() == 1) {
field.set(bean, candidates.values().iterator().next());
} else if (candidates.size() >1) {
String injectVal = field.getAnnotation(RountingInjected.class).value();
Object proxy = RoutingBeanProxyFactroy.createProxy(injectVal, type, candidates);
field.set(bean, proxy);
}
}
}
package com.chris.springboot2022.bean.processor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import java.util.Map;
import java.util.Objects;
public class RoutingBeanProxyFactroy {
private final static String DEFAULT_BEAN_NAME = "helloServiceImpl1";
public static Object createProxy(String name, Class<?> type, Map<String, ?> candidates) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setInterfaces(type);
proxyFactory.addAdvice(new VersionRoutingMethodInterceptor(name, candidates));
return proxyFactory.getProxy();
}
static class VersionRoutingMethodInterceptor implements MethodInterceptor {
private Object targetObject;
public VersionRoutingMethodInterceptor(String name, Map<String, ?> beans) {
this.targetObject = beans.get(name);
if (Objects.isNull(this.targetObject)) {
this.targetObject = beans.get(DEFAULT_BEAN_NAME);
}
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return invocation.getMethod().invoke(this.targetObject, invocation.getArguments());
}
}
}
@Test
public void testHelloWorldTest() {
helloServiceTest.getHelloService().sayHello();
Map<String, IHelloService> beansOfType = applicationContext.getBeansOfType(IHelloService.class);
if (CollUtil.isNotEmpty(beansOfType)) {
beansOfType.forEach((k, v) -> {
log.info("k:{}, v:{}", k, v.getClass().getName());
});
}
}
InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法。
public interface IHelloService extends InitializingBean {
void sayHello();
void initName();
}
@Configuration
public class InitBeanConfig {
@Bean(initMethod = "initName")
public IHelloService helloServiceImpl_1() {
return new HelloServiceImpl1();
}
@Bean(initMethod = "initName")
public IHelloService helloServiceImpl_2() {
return new HelloServiceImpl2();
}
}
注解形式的初始化使用过BeanPostProcessor实现的,spring会注册一个common的processor,然后再实例化bean以后,会调用所有的postProcessor,该common的processor就会扫描出被@PostConstruct标注的方法,然后调用之。
@Service
@Slf4j
public class HelloServiceImpl1 implements IHelloService {
public HelloServiceImpl1() {
log.info("construct HelloServiceImpl1 success");
}
private String name;
@Override
public void sayHello() {
System.out.println("hello, this is " + name);
}
@Override
public void afterPropertiesSet() {
this.name = "HelloServiceImpl1";
log.info("do afterPropertiesSet success, name:{}", name);
}
public void initName() {
this.name = "HelloServiceImpl1";
log.info("init name success, name:{}", name);
}
@PostConstruct
public void init() {
this.name = "HelloServiceImpl1";
log.info("do post construct success, name:{}", name);
}
}
construct
BeanPostProcessor > postProcessBeforeInitialization
PostConstruct
InitializingBean > afterPropertiesSet
init-method
BeanPostProcessor > postProcessAfterInitialization

2022-02-01 15:22:03.514 INFO 33596 --- [ main] c.c.c.b.p.s.impl.HelloServiceImpl1 : construct HelloServiceImpl1 success
2022-02-01 15:22:03.514 INFO 33596 --- [ main] c.c.c.b.p.HelloServiceInjectProcessor : begin postProcessBeforeInitialization, bean:com.chris.cloud.bean.processor.service.impl.HelloServiceImpl1@109a2025, name:helloServiceImpl_1
2022-02-01 15:22:03.514 INFO 33596 --- [ main] c.c.c.b.p.s.impl.HelloServiceImpl1 : do post construct success, name:HelloServiceImpl1
2022-02-01 15:22:03.514 INFO 33596 --- [ main] c.c.c.b.p.s.impl.HelloServiceImpl1 : do afterPropertiesSet success, name:HelloServiceImpl1
2022-02-01 15:22:03.515 INFO 33596 --- [ main] c.c.c.b.p.s.impl.HelloServiceImpl1 : init name success, name:HelloServiceImpl1
2022-02-01 15:22:03.515 INFO 33596 --- [ main] c.c.c.b.p.HelloServiceInjectProcessor : begin postProcessAfterInitialization, bean:com.chris.cloud.bean.processor.service.impl.HelloServiceImpl1@109a2025, name:helloServiceImpl_1
| 创建时间: | 2022/1/29 11:48 |
| 更新时间: | 2022/1/29 15:58 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/weixin_41540822/article/details/86606562#commentsedit |
@Builder
@Data
@Accessors(fluent = true)
@AllArgsConstructor
public class User {
private Integer id;
private final String zipCode = "215500";
private String userName;
private String password;
private List<String> hobbies;
}
生成相对略微复杂的构建器API。@Builder可以让你以下面显示的那样调用你的代码.
@Test
public void testBuilder() {
User user = User.builder()
.id(1212)
.userName("chris")
.password("12121")
.hobbies(Arrays.asList("reading", "running")).build();
log.info("user:{}", JSONUtil.toJsonStr(user));
}
在使用
@Singular注释注释一个集合字段(使用@Builder注释类),
lombok会将该构建器节点视为一个集合,并生成两个adder方法而不是setter方法。
一个向集合添加单个元素,一个将另一个集合的所有元素添加到集合中。
@Test
public void testBuilder() {
User user = User.builder()
.id(1212)
.userName("chris")
.password("12121")
.hobby("coding")
.hobby("teaching")
.hobbies(Arrays.asList("reading", "running")).build();
log.info("user:{}", JSONUtil.toJsonStr(user));
}
编译后的代码
public User.UserBuilder hobby(final String hobby) {
if (this.hobbies == null) {
this.hobbies = new ArrayList();
}
this.hobbies.add(hobby);
return this;
}
public User.UserBuilder hobbies(final Collection<? extends String> hobbies) {
if (hobbies == null) {
throw new NullPointerException("hobbies cannot be null");
} else {
if (this.hobbies == null) {
this.hobbies = new ArrayList();
}
this.hobbies.addAll(hobbies);
return this;
}
}
public User.UserBuilder clearHobbies() {
if (this.hobbies != null) {
this.hobbies.clear();
}
return this;
}
public User build() {
List hobbies;
switch(this.hobbies == null ? 0 : this.hobbies.size()) {
case 0:
hobbies = Collections.emptyList();
break;
case 1:
hobbies = Collections.singletonList(this.hobbies.get(0));
break;
default:
hobbies = Collections.unmodifiableList(new ArrayList(this.hobbies));
}
return new User(this.id, this.userName, this.password, hobbies);
}
* Using this annotation does nothing by itself; an annotation that makes lombok generate getters and setters,
* such as {@link lombok.Setter} or {@link lombok.Data} is also required.

将属性的getter和setter方法去掉前缀get和set,仅保留属性名称
如果没有设置chain,则chain会设置为true
如果为true, setter将返回
this而不是void
编译后的代码如下:

| 创建时间: | 2020/9/2 14:42 |
| 更新时间: | 2022/1/29 11:40 |
| 作者: | Chris |
/usr/share/vim/vim80# vi defaults.vim
将set mouse=a 改为 set mouse-=a

Err:1 http://mirrors.aliyun.com/ubuntu xenial InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
Err:2 http://mirrors.aliyun.com/ubuntu xenial-updates InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
Err:3 http://mirrors.aliyun.com/ubuntu xenial-backports InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
Err:4 http://mirrors.aliyun.com/ubuntu xenial-security InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
https://chrisjean.com/fix-apt-get-update-the-following-signatures-couldnt-be-verified-because-the-public-key-is-not-available/
将需要的key增加到apt-key 中
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
yml配置
# 配置日志输出级别
logging:
# 指定logback配置文件的位置
config: classpath:logback-spring.xml
# 文件日志要输出的路径
path: E:/logs/springboot_server
# 日志的输出级别
level:
root: info
java代码中通过
@Value("${logging.path}")来获取配置值
@Value("${logging.path}")
private String path;
# 拦截器路径拦截或者不拦截配置
interceptorconfig:
path:
#该路径下任何类型请求均拦截
include:
- /api/v1/token/api_token
- /api/v1/yibaotong/save
# 拦截器路径拦截或者不拦截配置
interceptorconfig:
path:
#或者可以写成:
include: [/api/v1/token/api_token,/api/v1/yibaotong/save]
定义list集合不能用
@value注解来获取list集合的所有值,
需要定义一个配置类bean,然后使用@ConfigurationProperties注解来获取list集合值,
做法如下:
@Data
@Component
@ConfigurationProperties(prefix = "interceptorconfig.path") // 配置文件的前缀
public class InterceptorPathBean
{
/*
* 需要拦截的路径
*/
private List<String> include;
}
首先创建一个user对象如下
@Data
public class User implements Serializable
{
private static final long serialVersionUID = 1L;
private String appId;
private String password;
}
然后yml配置文件的写法如下
jwt:
userlist:
- appId: YiBaoTong
password: 123456
- appId: ZhiKe
password: 123456
定义配置bean
@Data
@Component
@ConfigurationProperties(prefix = "jwt") // 配置 文件的前缀
public class JwtConfigBean
{
/**
* 用户列表
*/
private List<User> userlist;
}
interceptorconfig:
path:
includes: /api/v1,/api/v2 #注意要用逗号分隔开
可以通过
@value注解获取数组值
@Value("${interceptorconfig.path.includes}")
private String[] includes;
也可以通过创建配置类bean, 获取数组
@Data
@Component
@ConfigurationProperties(prefix = "interceptorconfig.path") // 配置 文件的前缀
public class InterceptorPathBean
{
private String[] includes;
}
interceptorconfig:
path:
maps: {name: 小明,age: 24}
interceptorconfig:
path:
#或者可以写成:
maps:
name: 小明
age: 24
通过创建配置类bean,获取map值
@Data
@Component
@ConfigurationProperties(prefix = "interceptorconfig.path") // 配置 文件的前缀
public class InterceptorPathBean
{
private Map<String , String> maps;
}
MySQL 支持 RFC 7159 定义的JSON规范,
主要有JSON 对象和JSON 数组两种类型
mysql :: mysql 8.0 reference manual :: 12.18.3 functions that search json values
{
"Image": {
"Width": 800,
"Height": 600,
"Title": "View from 15th Floor",
"Thumbnail": {
"Url": "http://www.example.com/image/xx9943",
"Height": 125,
"Width": 100
},
"IDs": [116, 943, 234, 38793]
}
}
[
{
"precision": "zip",
"Latitude": 37.7668,
"Longitude": -122.3959,
"Address": "",
"City": "SAN FRANCISCO",
"State": "CA",
"Zip": "94107",
"Country": "US"
},
{
"precision": "zip",
"Latitude": 37.371991,
"Longitude": -122.026020,
"Address": "",
"City": "SUNNYVALE",
"State": "CA",
"Zip": "94085",
"Country": "US"
}
]
- 本质上,JSON 是一种新的类型,有自己的存储格式,还能在每个对应的字段上创建索引,做特定的优化,这是传统字段串无法实现的
- 无须预定义字段,字段可以无限扩展。
而传统关系型数据库的列都需预先定义,想要扩展需要执行ALTER TABLE … ADD COLUMN …这样比较重的操作- JSON 类型是从 MySQL 5.7 版本开始支持的功能,而 8.0 版本解决了更新 JSON 的日志性能瓶颈。如果要在生产环境中使用 JSON 数据类型,强烈推荐使用 MySQL 8.0 版本。
在数据库中,JSON 类型比较适合存储一些修改较少、相对静态的数据
比如用户登录信息的存储如下
建表
DROP TABLE IF EXISTS UserLogin;
CREATE TABLE UserLogin (
userId BIGINT NOT NULL,
loginInfo JSON,
PRIMARY KEY(userId)
);
写数据
用户1 登录有三种方式:手机验证码登录、微信登录、QQ 登录,而用户2 只有手机验证码登录
INSERT INTO UserLogin VALUES (1, '{ "cellphone" : "1", "wxchat" : "码农", "77" :
"1" }');
INSERT INTO UserLogin VALUES (2, '{ "cellphone" : "1188" }');
查数据
SELECT
userId,
JSON_UNQUOTE(JSON_EXTRACT(loginInfo, "$.cellphone")) cellphone,
JSON_UNQUOTE(JSON_EXTRACT(loginInfo, "$.wxchat")) wxchat
FROM UserLogin;
+--------+-------------+--------------+
| userId | cellphone | wxchat |
+--------+-------------+--------------+
| 1 | 11 | 码农 |
| 2 | 11 | NULL |
+--------+-------------+--------------+
2 rows in set (0.01 sec)
当 JSON 数据量非常大,用户希望对 JSON 数据进行有效检索时,可以利用 MySQL 的
函数索引功能对 JSON 中的某个字段进行索引。
ALTER TABLE UserLogin ADD COLUMN cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone");
ALTER TABLE UserLogin ADD UNIQUE INDEX idx_cellphone(cellphone);
上述 SQL 首先创建了一个虚拟列
cellphone,这个列是由函数loginInfo->>"$.cellphone"计算得到的。然后在这个虚拟列上创建一个唯一索引idx_cellphone。
这时再通过虚拟列
cellphone进行查询,就可以看到优化器会使用到新创建的idx_cellphone索引:
EXPLAIN SELECT * FROM UserLogin
WHERE cellphone = '1'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: UserLogin
partitions: NULL
type: const
possible_keys: idx_cellphone
key: idx_cellphone
key_len: 1023
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
可以在一开始创建表的时候,就完成虚拟列及函数索引的创建
如下表创建的列cellphone对应的就是 JSON 中的内容,是个虚拟列;
uk_idx_cellphone就是在虚拟列 cellphone 上所创建的索引
CREATE TABLE UserLogin (
userId BIGINT,
loginInfo JSON,
cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone"),
PRIMARY KEY(userId),
UNIQUE KEY uk_idx_cellphone(cellphone)
);
CREATE TABLE Tags (
tagId bigint auto_increment,
tagName varchar(255) NOT NULL,
primary key(tagId)
);
SELECT * FROM Tags;
+-------+--------------+
| tagId | tagName |
+-------+--------------+
| 1 | 70后 |
| 2 | 80后 |
| 3 | 90后 |
| 4 | 00后 |
| 5 | 爱运动 |
| 6 | 高学历 |
| 7 | 小资 |
| 8 | 有房 |
| 9 | 有车 |
| 10 | 常看电影 |
| 11 | 爱网购 |
| 12 | 爱外卖 |
若不用 JSON 数据类型进行标签存储,通常会将用户标签通过字符串,加上分割符的方式,在一个字段中存取用户所有的标签:
这样做的缺点是:不好搜索特定画像的用户,另外分隔符也是一种自我约定,在数据库中其实可以任意存储其他数据,最终产生脏数据。

用 JSON 数据类型就能很好解决这个问题:
DROP TABLE IF EXISTS UserTag;
CREATE TABLE UserTag (
userId bigint NOT NULL,
userTags JSON,
PRIMARY KEY (userId)
);
INSERT INTO UserTag VALUES (1,'[2,6,8,10]');
INSERT INTO UserTag VALUES (2,'[3,10,12]');
MySQL 8.0.17 版本开始支持 Multi-Valued Indexes,用于在 JSON 数组上创建索引,并通过函数 member of、json_contains、json_overlaps 来快速检索索引数据。所以你可以在表 UserTag 上创建 Multi-Valued Indexes:
ALTER TABLE UserTagADD INDEX idx_user_tags ((cast((userTags->"$") as unsigned array)));
如果想要查询用户画像为常看电影的用户,可以使用函数 MEMBER OF:
EXPLAIN SELECT * FROM UserTag
WHERE 10 MEMBER OF(userTags->"$")\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: UserTag
partitions: NULL
type: ref
possible_keys: idx_user_tags
key: idx_user_tags
key_len: 9
ref: const
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
SELECT * FROM UserTag
WHERE 10 MEMBER OF(userTags->"$");
+--------+---------------+
| userId | userTags |
+--------+---------------+
| 1 | [2, 6, 8, 10] |
| 2 | [3, 10, 12] |
+--------+---------------+
2 rows in set (0.00 sec)
如果想要查询画像为 80 后,且常看电影的用户,可以使用函数 JSON_CONTAINS:
EXPLAIN SELECT * FROM UserTag
WHERE JSON_CONTAINS(userTags->"$", '[2,10]')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: UserTag
partitions: NULL
type: range
possible_keys: idx_user_tags
key: idx_user_tags
key_len: 9
ref: NULL
rows: 3
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
SELECT * FROM UserTag
WHERE JSON_CONTAINS(userTags->"$", '[2,10]');
+--------+---------------+
| userId | userTags |
+--------+---------------+
| 1 | [2, 6, 8, 10] |
+--------+---------------+
1 row in set (0.00 sec)
如果想要查询画像为 80 后、90 后,且常看电影的用户,则可以使用函数 JSON_OVERLAP:
EXPLAIN SELECT * FROM UserTag
WHERE JSON_OVERLAPS(userTags->"$", '[2,3,10]')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: UserTag
partitions: NULL
type: range
possible_keys: idx_user_tags
key: idx_user_tags
key_len: 9
ref: NULL
rows: 4
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
SELECT * FROM UserTag
WHERE JSON_OVERLAPS(userTags->"$", '[2,3,10]');
+--------+---------------+
| userId | userTags |
+--------+---------------+
| 1 | [2, 6, 8, 10] |
| 2 | [3, 10, 12] |
+--------+---------------+
2 rows in set (0.01 sec)
JSON 类型是 MySQL 5.7 版本新增的数据类型,用好 JSON 数据类型可以有效解决很多业务中实际问题。
作用在类、方法上
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Genericity<T> {
private T t1;
private T t2;
}
这就是泛型类的定义,通过在类名后面添加
<T>符号即可定义泛型类,而类中的属性类型均为T,这将导致类中的属性类型会跟随T的变化而变化,用法如下:
public static void main(String[] args) {
Genericity<Integer> genericity = new Genericity<>(1, 1);
Integer t1 = genericity.getT1();
Integer t2 = genericity.getT2();
int result = t1 + t2;
System.out.println(result);
}
实际上,泛型类可以定义多个泛型变量,比如:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Genericity<T,U> {
private T t1;
private U t2;
}
// 此时我们在创建对象时就可以传入两个类型:
public static void main(String[] args) {
Genericity<Integer,String> genericity = new Genericity<>(1, "1");
Integer t1 = genericity.getT1();
String t2 = genericity.getT2();
}
public static <T> void method(T t) {
}
编译器能够自动推断出泛型类型T
public static void main(String[] args) {
Genericity.method(1);
}
泛型方法的返回值也可以使用类型变量:
public static <T> T method(T t) {
return t;
}
泛型方法也支持定义多个类型变量:
public static <T, U> T method(T t, U u) {
return t;
}
public static <T> T min(T[] t) {
T minimum = t[0];
for (int i = 0; i < t.length; i++) {
if (minimum.compareTo(t[i]) > 0) {
minimum = t[i];
}
}
return minimum;
}
它能够取出数组t中的最小值,然而这段程序是有问题的,因为T可以是任意类型的对象,但不是什么对象都能够调用compareTo方法进行比较的,所以,我们需要对类型变量T进行限定,限定为实现了Comparable接口的对象,如下:
public static <T extends Comparable> T min(T[] t) {
T minimum = t[0];
for (int i = 0; i < t.length; i++) {
if (minimum.compareTo(t[i]) > 0) {
minimum = t[i];
}
}
return minimum;
}
一个泛型方法也可以对类型变量进行多个限定
public static <T extends Comparable & Serializable> T min(T[] t) {
......
}
泛型仅仅是在编译期间起作用,目的是让程序员在编译期间就避免发生一些类型不对应的问题,而在运行阶段,泛型是根本不存在的,因为虚拟机会对泛型进行擦除。
可以通过反射进行验证,因为反射是作用在运行阶段的:
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
list.add(1);
Method addMethod = list.getClass().getDeclaredMethod("add",Object.class);
addMethod.invoke(list,"2");
addMethod.invoke(list,true);
addMethod.invoke(list,3.2f);
System.out.println(list);
}
若是通过反射能够将这些非Integer类型的值存入list,则说明在运行期间确实是不存在泛型检查的,运行结果如下:
[1, 2, true, 3.2]
泛型擦除也体现在泛型方法中
public static <T extends Comparable> T min(T[] t) {
......
}
当程序运行期间,泛型会被擦除,此时方法变为如下:
public static Comparable min(Comparable t) {
......
}
这也就是为什么类型限定能够生效的原因了,通过泛型擦除后,该方法就只能接收和返回Comparable接口的实现类。
固定的泛型类型显然无法满足复杂多变的需求,为此,泛型设计者们还提供了泛型通配符,如:
//它表示类型变量必须是Person类型的子类
Genericity<? extends Person>
//此时类型变量就必须是Person类的超类
Genericity<? super Person>
还有一种情况是无限定通配符:
Genericity<?>
它和
Genericity<T>非常相似,但又有不同,Genericity<?>的setter方法不能被调用,getter方法只能返回Object类型,不过这种方式的用法较少,可能会被用来判断空引用:
public static boolean isNull(Genericity<?> genericity){
return genericity.getContent();
}
| 创建时间: | 2022/1/11 11:05 |
| 作者: | Chris |
| 创建时间: | 2022/1/7 17:31 |
| 更新时间: | 2022/1/11 10:48 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/lanxuezaipiao/p/3369962.html |
- 一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
- 然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。
换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
实现了Serilizable接口,在不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
反序列化后类中static型变量的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的
@Test
public void testTransient() {
User user = new User("Alexia", "123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getName());
System.err.println("password: " + user.getPassword());
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("C:/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("C:/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getName());
System.err.println("password: " + user.getPassword());
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}

@Test
public void testTransient() {
User user = new User("123456");
User.name = "Alexia";
System.out.println("read before Serializable: ");
System.out.println("username: " + User.name);
System.err.println("password: " + user.getPassword());
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("C:/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
// 在反序列化之前改变username的值
User.name = "Chris";
ObjectInputStream is = new ObjectInputStream(new FileInputStream("C:/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + User.name);
System.err.println("password: " + user.getPassword());
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
@Data
@AllArgsConstructor
public class User implements Serializable {
public static String name;
private transient String password;
}

| 创建时间: | 2020/12/29 19:57 |
| 更新时间: | 2020/12/29 20:04 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/maxiaoyin111111/article/details/81908172 |
在abc.txt中查找字符串abc的行。
find "abc" d:\abc.txt
/I 搜索字符串时忽略大小写。
find /i "abc" d:\abc.txt
参数/i代表的是“Ignore”(忽略),也就是忽略大小写。通过/I 参数可以不区分要查找的字符串“abc”的大小写。
/N 显示行号
find /n "abc" d:\abc.txt
参数/n代表英语单词“Number”(号码).。通过/n参数我们可以查找到字符串"abc"所在的行号。
要搜索包含单词 Windows 的当前目录和所有子目录中的每个文件,不考虑字母大小写,请键入如下
findstr /s /i Windows *.*
| 创建时间: | 2020/9/18 16:38 |
| 更新时间: | 2020/12/28 10:12 |
| 作者: | Chris |
services.msc

edit -> Visual NetWork Editor



cd /etc/sysconfig/network-scripts
cp ifcfg-ens33 ifcfg-ens33.bak
vi ifcfg-ens33
ens33是网卡的名字,这个在每台机器上可能是不同的。
修改内容如下:

ONBOOT="yes"
BOOTPROTO=static
IPADDR=192.168.174.127 #静态IP
GATEWAY=192.168.174.2 #默认网关, 与NAT设置中的网关一致
NETMASK=255.255.255.0 #子网掩码
DNS1=192.168.174.2 #DNS 配置与GATEWAY一致
DNS2=8.8.8.8 #DNS 配置


执行service network restart
| 创建时间: | 2020/12/23 18:07 |
| 更新时间: | 2020/12/23 18:25 |
| 作者: | Chris |
| 来源: | https://victor-huihui.gitee.io/2020/03/15/SpringBoot%E6%95%B4%E5%90%88redis/ |
SpringBoot集成Redis
实际上使用Spring Data Redis操作Redis
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hhzhu</groupId>
<artifactId>redis_practice</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
创建配置文件appliacation.yml
spring:
redis:
database: 0
host: localhost
port: 6379
package com.hhzhu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
package com.hhzhu.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
//实现序列化接口,否则无法存入redis
public class Student implements Serializable {
private Integer id;
private String name;
private Double score;
private Date birthday;
}
package com.hhzhu.controller;
import com.hhzhu.pojo.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@RestController
public class StudentHandler {
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/set")
//Request将json数据转换成java对象
public void set(@RequestBody Student student){
redisTemplate.opsForValue().set("student",student);
}
@GetMapping("/get/{key}")
public Student get(@PathVariable("key") String key){
return (Student) redisTemplate.opsForValue().get(key);
}
@DeleteMapping("/delete/{key}")
public boolean delete(@PathVariable("key") String key){
redisTemplate.delete(key);
return redisTemplate.hasKey(key);
}
}
@GetMapping("/string")
public String stringTest(){
redisTemplate.opsForValue().set("str","Hello world");
String str = (String) redisTemplate.opsForValue().get("str");
return str;
}
@GetMapping("list")
public List<String> listTest(){
ListOperations<String,String> listOperations = redisTemplate.opsForList();
listOperations.leftPush("list","hello");
listOperations.leftPush("list","world");
listOperations.rightPush("list","java");
//返回从左边开始前三个元素值
List<String> list = listOperations.range("list",0,2);
return list;
}
@GetMapping("/set")
public Set<String> setTest(){
SetOperations<String,String> setOperations = redisTemplate.opsForSet();
setOperations.add("set","Hello");
setOperations.add("set","Hello");
setOperations.add("set","world");
setOperations.add("set","world");
setOperations.add("set","java");
setOperations.add("set","java");
Set<String> set = setOperations.members("set");
return set;
}
@GetMapping("/zset")
public Set<String> zsetTest(){
ZSetOperations<String,String> zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset","Hello",1);
zSetOperations.add("zset","world",2);
zSetOperations.add("zset","java",3);
Set<String> set = zSetOperations.range("zset",0,2);
return set;
}
Hash:key value
HashOperations:key hashkey value
key是每一组数据的ID,hashkey和value是一组完整的HashMap数据,通过key来区分不同的HashMap

HashMap hashMap = new HashMap();
hashMap.put(key1,value1);
HashMap hashMap = new HashMap();
hashMap.put(key2,value2);
HashMap hashMap = new HashMap();
hashMap.put(key3,value3);
HashOperations<String,String,String> hashOperation = redisTemplate.opsForHash();
hashOperation.put(hashMap1,key1,value1);
hashOperation.put(hashMap2,key2,value2);
hashOperation.put(hashMap3,key3,value3);
@GetMapping("/hash")
public void hashTest(){
HashOperations<String,String,String> hashOperations = redisTemplate.opsForHash();
hashOperations.put("key","hashKey","Hello");
System.out.println(hashOperations.get("key","hashKey"));
}
| 创建时间: | 2020/12/17 15:57 |
| 更新时间: | 2020/12/18 16:08 |
| 作者: | Chris |
是Sybase公司开发的用于数据库设计的强大软件,是开发人员常用的数据库建模工具
新建一个pdm,dbms选择mysql

Database - Connect 选择数据库连接

配置连接信息
数据库连接这里是通过一个配置文件来获取连接信息的,首次的话因为没有,所以我们需要选择Configure进行配置。

填写配置信息


安装问题
cann't initialize javaVM
需要下载32位jdk并配置如下配置项
tools > General Options

Java Exception : Fatal Error. Unable to initialize DatabaseMetaData
Non SQL Error : Could not load class com.mysql.jdbc.Drive
而指定的jar包没有问题,那么是因为PowerDesigner无法找到驱动所产生的。解决办法是配置系统的classpath路径,指定jar包路径就好了

菜单选择,从数据库更新模型

选择数据库连接配置文件

选择对应的库和表

在表之间建立关联关系

Tools -> Display Preferences -> Table -> Columns
这个栏目中就是显示的列,就是控制模型显示的列;你可以全部去掉就只显示Name的值

去掉"Check model"复选框,如果不去掉的话,可能有些模型不规范报错

生成概念模型中只显示Entity名称

| 创建时间: | 2020/9/2 15:23 |
| 更新时间: | 2020/12/16 15:37 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/zndxall/archive/2018/09/04/9586088.html |
- 当正在dev分支上开发某个项目,这时项目中出现一个bug,需要紧急修复,但是正在开发的内容只是完成一半,还不想提交,这时可以用git stash命令将修改的内容保存至堆栈区,然后顺利切换到hotfix分支进行bug修复,修复完成后,再次切回到dev分支,从堆栈中恢复刚刚保存的内容。
- 由于疏忽,本应该在dev分支开发的内容,却在master上进行了开发,需要重新切回到dev分支上进行开发,可以用git stash将内容保存至堆栈中,切回到dev分支后,再次恢复内容即可。
总的来说,git stash命令的作用就是将目前还不想提交的但是已经修改的内容进行保存至堆栈中,后续可以在某个分支上恢复出堆栈中的内容。
这也就是说,stash中的内容不仅仅可以恢复到原先开发的分支,也可以恢复到其他任意指定的分支上。git stash作用的范围包括工作区和暂存区中的内容,也就是说没有提交的内容都会保存至堆栈中。
git stash save作用等同于git stash,
区别是执行存储时,添加备注,方便查找
git stash save “test1”
查看stash了哪些存储
git stash list
显示做了哪些改动,默认show第一个存储,如果要显示其他存贮,后面加stash@{$num},比如第二个 git stash show stash@{1}
git stash show
git stash show stash@{1}

显示第一个存储的改动详细内容,如果想显示其他存储,命令:git stash show stash@{$num} -p ,比如第二个:git stash show stash@{1} -p
git stash show -p
git stash show stash@{1} -p
应用某个存储,但不会把存储从存储列表中删除,默认使用第一个存储,即stash@{0},如果要使用其他个,git stash apply stash@{$num} , 比如第二个:git stash apply stash@{1}
git stash apply
git stash apply stash@{1}
恢复之前缓存的工作目录,将缓存堆栈中的对应stash删除,并将对应修改应用到当前的工作目录下, 默认为第一个stash,即stash@{0},如果要应用并删除其他stash,命令:git stash pop stash@{$num} ,比如应用并删除第二个:git stash pop stash@{1}
git stash pop
git stash pop stash@{1}
丢弃stash@{$num}存储,从列表中删除这个存储
git stash drop stash@{$num}
删除所有缓存的stash
git stash clear
| 创建时间: | 2020/10/14 21:48 |
| 更新时间: | 2020/12/14 16:46 |
| 作者: | Chris |
Redis
Remote Dicionary Server 远程字典服务器,是完全开源的C语言编写的高性能的KV分布式内存数据库。
Redis与其他KV产品有一下特点
http://redis.io/
http://www.redis.cn//
Linux 安装gcc
yum install gcc-c++
gcc -v
Ubuntu install gcc
apt install gcc
安装Redis
tar -zxvf redis-5.0.5.tar.gz
cd redis-5.0.5
make
make install
默认安装目录:/usr/local/bin
mkdir /myredis
cp /usr/local/redis/redis-5.0.5/redis.conf /myredis
vi /myredis/redis.conf
daemonize yes
redis 默认安装了16个DB
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16
redis-server /myredis/redis.conf
ps aux | grep redis
redis-cli -p 6379
ping
shundown
# 使用第三个库
127.0.0.1:6379> select 2
dbsize -->查看当前数据库的key的数量
keys *
keys k?
keys k*
exists k1 -> 判断k1是否存在,存在返回1,不存在返回0
move k1 2 -> 将k1移到3号库
expire k1 seconds -> 给指定的key设置过期时间, 到期后全将key移除
ttl k1 -> 查看还有多少秒过期,-1表示永不过期,-2表示已经过期,过期的key被移除。
type k1 -> 查看key的数据类型
del k1 -> 删除key
flushdb -->清空当前库中的key
flushall -->清空所有库中的key
(error) MISCONF Redis is configured to save RDB snapshots
vi /etc/sysctl.conf
vm.overcommit_memory = 1
二进制安全的,可以包含任何数据,比如图片和二进制的数据,一个redis字符串的value最多可以是512M.
append k1 12345
strlen k1
incr k1 -> 加1
decr k1 -> 减1
incrby k1 2 -> 加2
decrby k1 2 -> 减2
getrange k1 0 -1 / getrange k1 0 3
setrange k1 0 xxx
setex k1 10 v1 -> 新建k1并设置过期时间为10秒
setnx k1 v1 -> 不存在则set
mset k1 v1 k2 v2-> 一次性设置多个KV
mget k1 k2 -> 一次性获取多个K
msetnx k2 v2 k3 v3 -> 当KV不存在时,一次性设置多个KV, 如果有存在的K,则整体设置失败
是一个键值对集合,适合用于存储对象。
KV模式不变但V是一个键值对
hset user id 11
hget user id
hmset customer id 11 name li age 30
hmget customer id name age
hgetall customer
hdel user name -> 删除user 中的name
hlen customer
hexists customer id -> 判断customer中的key id是否存在,不存在返回0,存在返回1
hkeys customer
hvals customer
hincrby customer age 2
hincrbyfloat customer score 0.5
hsetnx customer email abc@163.com -> email不存在时才新增
是一个字符串链表,左右都可以插入数据,如果键不存在则新增链表,如果键存在则新增内容。
如果键全部移除则对应的链表则消失
lpush list01 1 2 3 4 5
lrange list01 0 -1
rpush list01 1 2 3 4 5
lpop list01
rpop list01
lindex list01 3 -> 获取索引是3的元素的值
llen list01
lrem list01 2 3-> 从list01里面删除2个3
del list01
ltrim list01 2 3 -> 截取指定索引范围2-3的值再赋给list01
rpoplpush source destination -> 将source的最后一个元素取出并压到dest的头元素
lset list01 index value -> 设置list01中index位置的value
linsert list01 before/after v x -> 在list01中的value前/后插入x
无序无重复
sadd set01 1 2 2 3 3
smembers set01 -> 打印整个集合
sismember set01 1 -> 判断1是否在set01中,在返回1,否则返回0
scard set01 -> 获取set中的元素个数
srem set01 3 -> 删除集合set01中的元素3
srandmember set01 3 ->在集合中随机抽取3个元素
spop set01 -> 随机弹出一个元素并移除
smove set01 set02 5 -> 把set01中的元素5移动到set02
sdiff set01 set02 -> 取set01有但set02中没有的元素
sunion set01 set02 -> 取两个集合的并集
sorted set, 不同的是每个元素关联一个double类型的score,通过score来为集合中的元素排序。
zadd zset01 60 v1 70 v2 80 v3 90 v4 100 v5
zrange zset01 0 -1 -> 获取集合中的元素值
zrange zset01 0 -1 withscores -> 获取集合中的元素值及其score
zrangebyscore zset01 60 90 -> 获取60<=score<=90的value
zrangebyscore zset01 60 (90 -> 获取60<=score<90的value, ( 表示不包含
zrangebyscore zset01 60 90 limit 2 3 -> 获取60<=score<=90的value并从index2截取3个
zrem zset01 v5
zcard zset01 -> 获取集合中元素的个数
zcount zset01 60 80 ->统计 60<=score<=80的value的个数
zrank zset01 v4 ->获取v4在集合中的位置的索引位
zscore zset01 v4 ->获取v4的score值
zrevrank zset01 v4 -> 逆序获取v4在集合中的位置的索引位
zrevrange zset01 0 -1 -> 逆序获取集合中的元素
zrevrangebyscore zset01 90 60 ->逆序获取60<=score>=90的value
zrevrangebyscore zset01 90 (60 limit 1 2
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
units are case insensitive so 1GB 1Gb 1gB are all the same.
tcp-backlog
timeout 0
tcp-keepalive 300
loglevel debug|verbose|notice|warning
logfile "/usr/local/redis/log/redis.log"
database 16
dir ./ -> 指定本地数据库的存放目录
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379>
config set requirepass '123456' -> 设置密码
auth 123456 -> 使用密码认证
config set requirepass '' -> 取消密码设置
config get dir -> 从那个目录下启动redis
maxclients 默认10000
maxmemory <bytes>
maxmemory-policy
volatile-lru -> Evict using approximated LRU among the keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key among the ones with an expire set.
allkeys-random -> Remove a random key, any key.
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
noeviction -> Don't evict anything, just return an error on write operations, set as default.
LRU 最近最久未访问的。如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。
LFU 最近最少使用算法。如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。
注意LFU和LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
举个简单的例子:
假设缓存大小为3,数据访问序列为
set(2,2),set(1,1)
get(2),get(1),get(2)
set(3,3),set(4,4),
则在set(4,4)时对于LFU算法应该淘汰(3,3),而LRU应该淘汰(1,1)。
在指定时间间隔内将数据集快照写入磁盘,恢复时直接将snapshot快照读入内存。
单独创建一个fork一个子进程进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束会,再用这个临时文件代替上次持久化好的文件,整个过程中主进程不进行任何IO操作,这就确保了极高的性能。
如果要进行大规模的数据恢复,且对数据恢复的完整性要求不是很高,那RDB比AOF的方式更为高效。
Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量,环境变量,程序计数器)与原进程一致,但是它是一个全新的进程并作为原进程的子进程。
1. RDB的缺点是最后一次持久化后的数据可能丢失
2. 当主程序数据量很大时,fork一个子进得非常消耗资源,相当于将原进程double一份。
RDB保存是的dump.rdb文件
save <seconds> <changes>
save 900 1 -> after 900 sec (15 min) if at least 1 key changed
save 300 10 -> after 300 sec (5 min) if at least 10 keys changed
save 60 10000 -> after 60 sec if at least 10000 keys changed
使用save或bgsave命令手动即刻备份
save 只管保存,在保存时其它线程阻塞,在生产环境很少执行 SAVE 操作
bgsave fock一个子进程异步进行快照操作,同时父进程还可以响应当前客户端请求.可以通过 lastsave 获了最后一次成功执行快照的时间来判断bgsave是否成功。
7.1.5 相关配置项
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes -> 使用CRC64算法校验数据完整性
dbfilename dump.rdb
以日志的形式记录每个写操作,只许追加文件但不能改写文件,redis启动时将日志文件内容从头到尾执行一次完成数据恢复。
aof文件不断增长变大,文件远大于rdb,恢复速度慢于rdb,运行效率慢于rdb。
每次load aof文件时带来持续的IO。
appendonly no -> 要用aof,需要改为yes
appendfilename
appendfsync : aof备份
always -> 同步持久化,每一个数据变更都会被立刻记录到磁盘
everysec -> 默认设置,异步操作每秒记录一,如果一秒内宕机会有数据丢失
no
是什么
appendonly.aof文件采用文件追加的方式,文件会变得越来越大,当文件大小超过设置的阈值时,Redis会启动AOF内容的压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof。
重写原理
AOF文件持续增长变大,会fork出一条新进程将文件重写,先写临时文件再rename,重写aof文件时并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件。
触发机制
redis会记录上次重写时的aof文件的大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且大于64M时触发.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
./redis-check-aof --fix appendonly.aof
yes

一次性执行多个命令,本质是一组命令的集合,一个事务中的所有命令都会被序列化,按顺序串行化执行而不允许其它命令加塞
multi 标记一个事务开启,并返回ok.
exec 被redis调用,来执行一个事务中入队的多个命令
discard 放弃本次事务
unwatch 取消对所有key的监视
watch k1 k2... 监视一个或者多个key,如果在事务执行exec之前这些被监控的key被其它命令改动,那么事务将被打断,一但执行了exec之后,之前加的监控锁都会被清理掉。
watch监控:
行级锁:
表级锁:迸发时极差,但一致性是好
悲观锁:
乐观锁
CAS check-and-set
在编译过程中出错时,整个事务中的所有命令集体失败。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> setget k3 33
(error) ERR unknown command `setget`, with args beginning with: `k3`, `33`,
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty list or set)
在执行的过程中出错时,只有出错的命令失败,其它命令正常执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) "v1"
3) "v2"
4) OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby balance 20
QUEUED
127.0.0.1:6379> incrby debt 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
也就是我们所说的主从复制,主机数据更新后根据配置和策略自动同步到备机的机制, Master以写为主,Slave以读为主
salve启动成功连接到master后会发关一个sync命令,master将整个数据文件全量发送到slave,此后master新增的数据会增量复制到slaver, 但是只要重新连接一次master就会首先做一次全量复制。
复制最大的缺点就是延时,因为所的有写操作都在master上,然后同步更新到slaver上,所以master同步到slaver会有一定的延时。
读写分离,容灾恢复
配从不配主
备机配置
slaveof master-ip master-portnumber
每次与master断开之后,都需要重新连接,除非你配置到redis.conf文件
修改配置文件
拷贝多个redis.conf文件
开启daemonize yes
Pid文件名称
指定端口
Log 文件名称
Dump.rdb 名称
一主二仆
127.0.0.1:6379> info replication
master:
./redis-server /myredis/redis_6379.conf
./redis-cli -p 6379
slave01:
./redis-server /myredis/redis_6380.conf
./redis-cli -p 6380
slaveof 127.0.0.1 6379
slave02:
./redis-server /myredis/redis_6381.conf
./redis-cli -p 6381
slaveof 127.0.0.1 6379
只有master节点可以写,salve节点不能写只能读取
如果主机宕机,备机原地待命

薪火相传(去中心化)
上一个slave可以是下一个slave的master 来接收其它连接和请求,这样可以有效的减轻链条中master的写压力。
127.0.0.1:6379> info replication
master:
./redis-server /myredis/redis_6379.conf
./redis-cli -p 6379
slave01:
./redis-server /myredis/redis_6380.conf
./redis-cli -p 6380
slaveof 127.0.0.1 6379
slave02:
./redis-server /myredis/redis_6381.conf
./redis-cli -p 6381
slaveof 127.0.0.1 6380

反客为主(当master挂掉之后,从新分配)
127.0.0.1:6379> info replication
master:
./redis-server /myredis/redis_6379.conf
./redis-cli -p 6379
shundown
exit
slave01:
./redis-server /myredis/redis_6380.conf
./redis-cli -p 6380
slaveof no one
slave02:
./redis-server /myredis/redis_6381.conf
./redis-cli -p 6381
slaveof 127.0.0.1 6380
是反客为主的自动版本,从后台自动监控主机是否故障,主要发生故障后自动将从机转为主机。
cd /myredis
vi sentinel.conf
sentinel monitor masterhost 本机ip master端口号 1
-- 当主机宕机之后,让slaver投票,谁的票数多,谁就成主机
cd /usr/local/bin
./redis-sentinel /myredis/sentinel.conf
当6379shutdown后,sentinal会启动leader选举机制,之后6379重启后会作用新的master的salve
2956:X 22 May 2019 22:54:25.383 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2956:X 22 May 2019 22:54:25.393 # Sentinel ID is f0a3e6562c5bd63b41f9445fa7b9cfef4c34c2f3
2956:X 22 May 2019 22:54:25.393 # +monitor master host6379 127.0.0.1 6379 quorum 1
2956:X 22 May 2019 22:54:25.394 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:54:25.396 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.279 # +sdown master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.279 # +odown master host6379 127.0.0.1 6379 #quorum 1/1
2956:X 22 May 2019 22:56:01.279 # +new-epoch 1
2956:X 22 May 2019 22:56:01.279 # +try-failover master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.281 # +vote-for-leader f0a3e6562c5bd63b41f9445fa7b9cfef4c34c2f3 1
2956:X 22 May 2019 22:56:01.281 # +elected-leader master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.281 # +failover-state-select-slave master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.343 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.343 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.399 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.626 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.626 # +failover-state-reconf-slaves master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.708 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:01.780 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:02.819 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:02.897 # +failover-end master host6379 127.0.0.1 6379
2956:X 22 May 2019 22:56:02.897 # +switch-master host6379 127.0.0.1 6379 127.0.0.1 6380
2956:X 22 May 2019 22:56:02.898 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ host6379 127.0.0.1 6380
2956:X 22 May 2019 22:56:02.898 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ host6379 127.0.0.1 6380
2956:X 22 May 2019 22:56:02.898 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ host6379 127.0.0.1 6380
2956:X 22 May 2019 22:56:32.906 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ host6379 127.0.0.1 6380
2956:X 22 May 2019 23:03:17.541 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ host6379 127.0.0.1 6380
2956:X 22 May 2019 23:03:27.455 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ host6379 127.0.0.1 6380
| 创建时间: | 2020/11/13 17:08 |
| 更新时间: | 2020/11/13 17:08 |
| 作者: | Chris |
https://www.cnblogs.com/jpfss/p/11075458.html
| 创建时间: | 2020/10/28 14:24 |
| 更新时间: | 2020/11/7 22:46 |
| 作者: | Chris |
Dubbo
https://blog.csdn.net/qq_41157588/article/details/106737191
<<分布式系统原理与范型>>
对系统应用进行垂直拆分
分布式服务架构下主要解决的是如何很好的进行RPC

看一个RPC构架能否快速在两个服务器间建立连接。
传的是xml,json 还是二进制流

Dubbo, gRPC, Thrift, HSF(High Speed Service Framework)
基于访问压力,实时管理集群容量,提高集群利用率、
流动计算架构

一款高性能的 Java RPC框架
面向接口代理的高性能RPC调用
智能负载均衡
服务注册与发现
高度可扩展能力
基于微内核+插件的设计原则
运行期流量调度
可以在运行时配置Dubbo,以便可以根据不同的规则对流量进行路由,这使得轻松支持诸如蓝绿部署,数据中心感知路由等功能成为可能。
可视化的服务治理与运维

dubbo推荐使用zk作为服务注册中心
安装参见 zk installment
https://github.com/apache/dubbo
dubbo admin是dubbo的控制台,具有服务查询、服务治理的功能。
最新版的dubbo admin做了前后端的分离,前端使用Vue、Vuetify分别作为Javascript框架和UI框架,后端采用Spring Boot框架。
https://github.com/apache/dubbo-admin
下载zip包

配置dubbo-admin-server
配置文件修改zookeeper地址,dubbo控制台端口默认8080,可以自定义端口

打包
在主目录dubbo-admin-develop目录下,执行 mvn clean package
运行jar包
cd dubbo-admin-distribution/target
java -jar dubbo-admin-0.2.0-SNAPSHOT.jar


测试

建module
user-service-provider
改pom
<dependencies>
<dependency>
<groupId>com.chris.dubbo</groupId>
<artifactId>dubbo-api-common</artifactId>
</dependency>
<!--引入dubbo-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
</dependencies>
接口类
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1, address2);
}
}
建spring configation file : provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!--指定当前服务名称,不要和别的服务名称重复 -->
<dubbo:application name="user-service-consumer"/>
<!--指定注册中心位置 -->
<dubbo:registry address="zookeeper://master:2181"/>
<!--<dubbo:registry protocol="zookeeper" address="master:2181"/>-->
<!--指定dubbo协议在20080端口暴露服务-->
<dubbo:protocol name="dubbo" port="20080"/>
<!--声明要暴露的服务
interface : 需要暴露的服务的接口全名,从dubbo-api-common中的com.chris.dubbo.service获取
ref : 指向服务的实现实例对象
-->
<dubbo:service interface="com.chris.dubbo.service.UserService" ref="userServiceImpl"/>
<bean id="userServiceImpl" class="com.chris.user.service.impl.UserServiceImpl"/>
</beans>
测试类
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
public class MainTest {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("provider.xml");
ioc.start();
System.in.read();
}
}
建module
order-service-consumer
改pom
<dependencies>
<dependency>
<groupId>com.chris.dubbo</groupId>
<artifactId>dubbo-api-common</artifactId>
</dependency>
<!--引入dubbo-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
</dependencies>
接口类
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private UserService userService;
@Override
public List<UserAddress> initOrder(String userId) {
System.out.println("用户id:" + userId);
//1、查询用户的收货地址
List<UserAddress> addressList = userService.getUserAddressList(userId);
for (UserAddress userAddress : addressList) {
System.out.println(userAddress.getUserAddress());
}
return addressList;
}
}
建spring configation file : consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.chris.order.service.impl"/>
<!--指定当前服务名称,不要和别的服务名称重复 -->
<dubbo:application name="order-service-consumer"/>
<!--指定注册中心位置 -->
<dubbo:registry address="zookeeper://master:2181"/>
<!--<dubbo:registry protocol="zookeeper" address="master:2181"/>-->
<!--指定dubbo协议在20080端口暴露服务-->
<dubbo:protocol name="dubbo" port="10080"/>
<!-- 声明要调用的远程服务的接口:生成远程服务代理 -->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService"/>
</beans>
测试类
package com.chris.order;
import com.chris.dubbo.service.OrderService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
public class MainTest {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("consumer.xml");
OrderService orderService = applicationContext.getBean(OrderService.class);
orderService.initOrder("1");
System.in.read();
}
}
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。

建module
dubbo-api-common
对象类
com.chris.dubbo.bean
com.chris.dubbo.bean.UserAddress
接口类
com.chris.dubbo.service
com.chris.dubbo.service.OrderService
com.chris.dubbo.service.UserService
https://github.com/apache/dubbo-spring-boot-project
https://github.com/apache/dubbo-spring-boot-project/tree/0.2.x
建module
boot-user-service-provider
改pom
<!--引入spring-boot dubbo-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--引入dubbo-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
写yml
dubbo:
application:
name: boot-user-service-provider
registry:
protocol: zookeeper
address: master:2181
protocol:
name: dubbo
port: 20081
主启动
@SpringBootApplication
@EnableDubbo //开启基于注解的dubbo功能
public class OrderMain {
public static void main(String[] args) {
SpringApplication.run(OrderMain.class, args);
}
}
业务类
package com.chris.user.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.chris.dubbo.bean.UserAddress;
import com.chris.dubbo.service.UserService;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Service //暴露服务
@Component
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1, address2);
}
}
建module
boot-order-service-consumer
改pom
<!--引入spring-boot dubbo-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--引入dubbo-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
写yml
server:
port: 8081
dubbo:
application:
name: boot-order-service-consumer
registry:
protocol: zookeeper
address: master:2181
check: false #停用启动时检查服务注册中心是否存在
业务类
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Resource
private OrderService orderService;
@ResponseBody
@RequestMapping("/initOrder/{userId}")
public List<UserAddress> initOrder(@PathVariable("userId") String userId) {
return orderService.initOrder(userId);
}
}
接口类
package com.chris.order.service.impl;
import com.alibaba.dubbo.config.annotation.Reference;
import com.chris.dubbo.bean.UserAddress;
import com.chris.dubbo.service.OrderService;
import com.chris.dubbo.service.UserService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderServiceImpl implements OrderService {
@Reference(check = false)
private UserService userService;
@Override
public List<UserAddress> initOrder(String userId) {
System.out.println("用户id:" + userId);
//1、查询用户的收货地址
List<UserAddress> addressList = userService.getUserAddressList(userId);
for (UserAddress userAddress : addressList) {
System.out.println(userAddress.getUserAddress());
}
return addressList;
}
}
4.4.3 测试
http://localhost:8081/order/initOrder/1
[
{
"id": 1,
"userAddress": "北京市昌平区宏福科技园综合楼3层",
"userId": "1",
"consignee": "李老师",
"phoneNum": "010-56253825",
"isDefault": "Y"
},
{
"id": 2,
"userAddress": "深圳市宝安区西部硅谷大厦B座3层(深圳分校)",
"userId": "1",
"consignee": "王老师",
"phoneNum": "010-56253825",
"isDefault": "N"
}
]
http://dubbo.apache.org/en-us/docs/user/configuration/xml.html
http://dubbo.apache.org/en-us/docs/user/references/xml/introduction.html
配置文件覆盖策略
先是JVM中配置的参数
再是application.yml或application.properties中配置的参数
最后是dubbo.properties中配置的dubbo公共参数
http://dubbo.apache.org/en-us/docs/user/configuration/properties.html

配置参数覆盖策略
局部优先(方法级优先,接口级次之,全局配置再次之)
消费者优先(如果级别一样,则消费方优先,提供方次之)
注:如果级别不一样,局部优先
例如:在消费端配置的超时为接口上的5秒,而在服务提供端配置方法级的超时时间为2秒,则服务提供端配置方法级的超时时间为2秒会被优先选择

消费者启动时检查
http://dubbo.apache.org/en-us/docs/user/demos/preflight-check.html
消息者启动时默认检查是否存在服务提供者, 如果没有服务提供者,启动消息者时会报错是
public class MainTest {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("consumer.xml");
/*OrderService orderService = applicationContext.getBean(OrderService.class);
orderService.initOrder("1");*/
System.in.read();
}
}
consumer.xml
配置某个服务启动时是否检查
<!-- 声明要调用的远程服务的接口:生成远程服务代理
check: 检查消费者依赖的服务提供者是否存在,默认为true,即检查,等同于<dubbo:consumer check="false"/>
-->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService" check="false"/>
consumer.xml
配置所有服务启动时是否检查
http://dubbo.apache.org/en-us/docs/user/references/xml/dubbo-consumer.html
<!--配置当前消费者的统一规则,当前所有的服务都不作启动时检查-->
<dubbo:consumer check="false"/>
与springboot集成后可以在注解中指定
@Reference(check = false)
private UserService userService;
注册中心启动时检查
consumer.xml
<!--指定注册中心位置
check: 检查注册中心是否存在,默认为true,即检查/>
-->
<dubbo:registry address="zookeeper://master:2181" check="false"/>
与springboot集成后可以在application.yml中配置
dubbo:
application:
name: boot-order-service-provider
registry:
protocol: zookeeper
address: master:2181
check: false
消费者默认超时时间
在没有给消费者设置超时时间的情况下,消费者启动时已超时

因为dubbo:reference 的timeout 用的是消费者统一配置dubbo:consumer的timeout 默认为1秒超时


消费端超时配置
消费端consumer.xml
<!-- 声明要调用的远程服务的接口:生成远程服务代理
check: 检查消费者依赖的服务提供者是否存在,默认为true,即检查,check="false"等同于<dubbo:consumer check="false"/>
timeout:用的是消费者统一配置dubbo:consumer的timeout 默认为1秒超时
dubbo:method: 指向服务提供方暴露的某个方法,timeout对此方法设置在消费端的等待时间
-->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService" check="false" timeout="7000">
<dubbo:method name="getUserAddressList" timeout="5000"/>
</dubbo:reference>
<!--配置当前消费者的全局统一规则,
check:设置当前消费端所有的服务是否启动时检查
timeout:设置当前消费端所有服务的超时时间
-->
<dubbo:consumer check="false" timeout="2000"/>
服务端超时配置
服务端provicer.xml
<!--声明要暴露的服务
interface : 需要暴露的服务的接口全名,从dubbo-api-common中的com.chris.dubbo.service获取
ref : 指向服务的实现实例对象
timeout:设置服务提供者的超时时间,默认为1秒超时
dubbo:method: 指向服务提供者暴露的某个方法,timeout对此方法设置在服务提供端的等待时间
-->
<dubbo:service interface="com.chris.dubbo.service.UserService" ref="userServiceImpl" timeout="1000">
<dubbo:method name="getUserAddressList" timeout="2000"/>
</dubbo:service>
<bean id="userServiceImpl" class="com.chris.user.service.impl.UserServiceImpl"/>
配置消费端
comsumer.xml
retries: 服务重试次数,不包括第一次调用, 如果 retries="3"表示第一次调用失败后会再调用三次
<!-- 声明要调用的远程服务的接口:生成远程服务代理
check: 检查消费者依赖的服务提供者是否存在,默认为true,即检查,check="false"等同于<dubbo:consumer check="false"/>
timeout:用的是消费者统一配置dubbo:consumer的timeout 默认为1秒超时
dubbo:method: 指向服务提供方暴露的某个方法,timeout对此方法设置在消费端的等待时间
retries: 服务重试次数,不包括第一次调用, 如果 retries="3"表示第一次调用失败后会再调用三次
-->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService" check="false" timeout="7000"
retries="3">
<!--<dubbo:method name="getUserAddressList" timeout="5000"/>-->
</dubbo:reference>
配置服务端
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("retry signal.....1....");
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Arrays.asList(address1, address2);
}
}
测试
在服务端的后台会打印如下信息

如果有多个服务端节点,消费者在重试时会使用带有负载均衡的重试机制,即每个服务端节点轮询调用
幂等性和非幂等性下的重试
修改,删除,修改的操作一般为幂等操作,执行多次后最终结果一致,可以允许重试
新增为非幂等操作,多次执行同一操作可能会产生多条数据,不能允许重试
http://dubbo.apache.org/zh-cn/docs/user/demos/multi-versions.html
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
- 在低压力时间段,先升级一半提供者为新版本
- 再将所有消费者升级为新版本
- 然后将剩下的一半提供者升级为新版本
服务端
provider.xml
version: 指定接口的版本号来支持灰度发布
<!--声明要暴露的服务
interface : 需要暴露的服务的接口全名,从dubbo-api-common中的com.chris.dubbo.service获取
ref : 指向服务的实现实例对象
timeout:设置服务提供者的超时时间,默认为1秒超时
dubbo:method: 指向服务提供者暴露的某个方法,timeout对此方法设置在服务提供端的等待时间
version: 指定接口的版本号来支持灰度发布
-->
<dubbo:service interface="com.chris.dubbo.service.UserService" ref="userServiceImpl" timeout="1000" version="0.0.1">
<dubbo:method name="getUserAddressList" timeout="3000"/>
</dubbo:service>
<bean id="userServiceImpl" class="com.chris.user.service.impl.UserServiceImpl"/>
<dubbo:service interface="com.chris.dubbo.service.UserService" ref="userServiceImpl2" timeout="1000"
version="0.0.2">
<dubbo:method name="getUserAddressList" timeout="3000"/>
</dubbo:service>
UserServiceImpl
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("retry signal.....old....");
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Arrays.asList(address1, address2);
}
}
复制UserServiceImpl为UserServiceImpl2
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("retry signal.....new....");
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Arrays.asList(address1, address2);
}
}
消费端
consumer.xml
version:声名使用服务提供者的哪个版本的接口, version=“*” 表示随机调用接口的不同版本
<!-- 声明要调用的远程服务的接口:生成远程服务代理
check: 检查消费者依赖的服务提供者是否存在,默认为true,即检查,check="false"等同于<dubbo:consumer check="false"/>
timeout:用的是消费者统一配置dubbo:consumer的timeout 默认为1秒超时
dubbo:method: 指向服务提供方暴露的某个方法,timeout对此方法设置在消费端的等待时间
retries: 服务重试次数,不包括第一次调用, 如果 retries="3"表示第一次调用失败后会再调用三次
version:声名使用服务提供者的哪个版本的接口,version=“*” 表示随机调用接口的不同版本
-->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService" check="false" timeout="7000" retries="3" version="0.0.1">
<!--<dubbo:method name="getUserAddressList" timeout="5000"/>-->
</dubbo:reference>
测试
当消费端version="0.0.1"
<dubbo:reference interface="com.chris.dubbo.service.UserService" check="false" timeout="7000" retries="3" version="0.0.1">
在服务端的后台会打印如下信息

当消费端version="0.0.2"
<dubbo:reference interface="com.chris.dubbo.service.UserService" check="false" timeout="7000" retries="3" version="0.0.2">
在服务端的后台会打印如下信息

可以用来在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
实现UserServiceStub
@RequiredArgsConstructor
public class UserServiceStub implements UserService {
private final UserService userService;
@Override
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("invoke UserServiceStub");
if (StrUtil.isNotEmpty(userId)) {
return userService.getUserAddressList(userId);
}
return null;
}
}

配置consumer.xml
!-- interface:声明要调用的远程服务的接口:生成远程服务代理
check: 检查消费者依赖的服务提供者是否存在,默认为true,即检查,check="false"等同于<dubbo:consumer check="false"/>
timeout:用的是消费者统一配置dubbo:consumer的timeout 默认为1秒超时
dubbo:method: 指向服务提供方暴露的某个方法,timeout对此方法设置在消费端的等待时间
retries: 服务重试次数,不包括第一次调用, 如果 retries="3"表示第一次调用失败后会再调用三次
version:声名使用服务提供者的哪个版本的接口
stub :指向引用接口UserService的本地存根接口UserServiceStub
-->
<dubbo:reference id="userService" interface="com.chris.dubbo.service.UserService" check="false" timeout="7000"
retries="3" version="0.0.2" stub="com.chris.dubbo.service.stub.UserServiceStub">
<!--<dubbo:method name="getUserAddressList" timeout="5000"/>-->
</dubbo:reference>
测试
在order-servicer-consumer中可以看到如下消息,表示在调用UserService接口方法前先调用存根接口UserServiceStub

直接集成
建module
改pom
<!--引入spring-boot dubbo-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--引入dubbo-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
dubbo 2.6以前的版本引入zkclient操作zookeeper
dubbo 2.6及以后的版本引入curator操作zookeeper
下面两个zk客户端根据dubbo版本2选1即可
-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
写yml
server:
port: 8081
dubbo:
application:
name: boot-order-service-provider
registry:
protocol: zookeeper
address: master:2181
check: false #停用启动时检查服务注册中心是否存在
主启动加@EnableDubbo //开启基于注解的dubbo功能
消费端引用服务@Reference
@Reference(check = false)
private UserService userService;
服务端服务使用dubbo 注解@Service 暴露服务
import com.chris.dubbo.service.UserService;
import org.springframework.stereotype.Component;
@Service //暴露服务
@Component
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1, address2);
}
}
使用xml配置信息
将provider.xml或consumer.xml 放在 resources目前下
主启动类使用 @ImportResource(locations = "classpath:provider.xml")
@SpringBootApplication @ImportResource(locations = "classpath:provider.xml") public class UserMain { public static void main(String[] args) { SpringApplication.run(UserMain.class, args); } }
使用配置类来配置dubbo信息
DubboConfig
package com.chris.user.config;
import com.alibaba.dubbo.config.*;
import com.chris.dubbo.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
@Configuration
public class DubboConfig {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("boot-user-service-provider");
return applicationConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setProtocol("zookeeper");
registryConfig.setAddress("master:2181");
return registryConfig;
}
@Bean
public ProtocolConfig protocolConfig() {
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20081);
return protocolConfig;
}
/*
<dubbo:service interface="com.chris.dubbo.service.UserService" ref="userServiceImpl" timeout="1000" version="0.0.1">
<dubbo:method name="getUserAddressList" timeout="3000"/>
</dubbo:service>
*/
@Bean
public ServiceConfig<UserService> userServiceConfig(UserService userService) {
ServiceConfig<UserService> serviceConfig = new ServiceConfig<>();
serviceConfig.setInterface(UserService.class);
serviceConfig.setRef(userService);
serviceConfig.setTimeout(5000);
serviceConfig.setVersion("0.0.1");
MethodConfig methodConfig = new MethodConfig();
methodConfig.setName("getUserAddressList");
methodConfig.setTimeout(3000);
serviceConfig.setMethods(Collections.singletonList(methodConfig));
return serviceConfig;
}
/*
<dubbo:provider timeout="3000"/>
*/
@Bean
public ProviderConfig providerConfig() {
ProviderConfig providerConfig = new ProviderConfig();
providerConfig.setTimeout(3000);
return providerConfig;
}
}
服务端接口类使用dubbo 注解@Service 暴露服务
import com.alibaba.dubbo.config.annotation.Service;
import com.chris.dubbo.bean.UserAddress;
import com.chris.dubbo.service.UserService;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Service //暴露服务
@Component
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1, address2);
}
}
现象:zookeeper注册中心宕机,还可以消费dubbo暴露的服务。
健壮性
l 监控中心宕掉不影响使用,只是丢失部分采样数据
l 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
l 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
l 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
l 服务提供者无状态,任意一台宕掉后,不影响使用
l 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
不借助注册中心,实现消费者直接访问服务提供者
@Reference(check = false, url = "127.0.0.1:20081")
package com.chris.order.service.impl;
import com.alibaba.dubbo.config.annotation.Reference;
import com.chris.dubbo.bean.UserAddress;
import com.chris.dubbo.service.OrderService;
import com.chris.dubbo.service.UserService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderServiceImpl implements OrderService {
@Reference(check = false, url = "127.0.0.1:20081")
private UserService userService;
@Override
public List<UserAddress> initOrder(String userId) {
System.out.println("用户id:" + userId);
//1、查询用户的收货地址
List<UserAddress> addressList = userService.getUserAddressList(userId);
for (UserAddress userAddress : addressList) {
System.out.println(userAddress.getUserAddress());
}
return addressList;
}
}
http://dubbo.apache.org/zh-cn/docs/user/demos/loadbalance.html
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
/**
* random load balance.
*
*/
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
Ctrl+H 打开继承树

Random LoadBalance

基于权重的随机负载均衡策略。
下一次调用无法确定请求会落在哪个服务节点上,但是总概率是按权重来分布的。
RoundRobin LoadBalance

基于权重的轮询负载均衡策略。
下一次调用请求会落在有序的服务节点上,但是需要考虑到权重分布。
LeastActive LoadBalance

最少活跃数负载均衡,活跃数指调用前后的响应时间, 下次在调用服务之前会比对上次调用所花费的时间,优先调用上次响应时间最少的服务节点。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
ConsistentHash LoadBalance
一致性 Hash负载均衡策略
相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
<dubbo:service interface="..." loadbalance="roundrobin" />
<dubbo:reference interface="..." loadbalance="roundrobin" />
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
在暴露服务时直接hardcode在@Service里面
@Service(weight = 100) //暴露服务
@Component
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1, address2);
}
}
在控制台动态增减权重

| 创建时间: | 2022/1/5 10:50 |
| 更新时间: | 2022/1/5 11:40 |
| 作者: | Chris |
| 来源: | file:///C:/Program%20Files%20(x86)/TeamTalk2.0/resources/app.asar/build/renderer/windows/dashboard.html?isSingleLogin=false#/chat?groupId=4648197 |
- 上午时间9:00-12:00
- 就餐时间12:00-14:00
- 下午时间14:00-18:30
- 要求每天【不包括周末,节假日】在OMS上打卡,每天两次,上下班各一次。
- 如果忘记打卡或迟到,OMS打卡会标红即异常打卡。
- 因特殊原因无法在OMS正常打卡,需要在客户TT打卡群中发送
现场上班打卡【已iknow健康打卡】
- 请假或调休需提交一天向客户报备并提交相关书面申请
- 不许在手头工作没有完成或没有交待清楚的情况下请假或调休。
- 在上线前一天尽量不要请假或调休,如果事件紧急需向客户方负责人说明原因, 在得到明确许可情况下方可请假或调休。
邮件发送格式
发送:wangshun@oppo.com;yangyuxiang@oppo.com
抄送:zhengcan1@oppo.com;lilun@e-lead.cn; 和对应项目的对应项目BA、SE邮箱
OMS提交申请
在OMS管理系统发起请假/补签/加班申请等流程时,注意读者添加:
王顺、闫伟、对应项目BA、SE
PLM微信打卡群发送申请告知
每次调休或请假都需要在PLM微信打卡群里面
@王顺,郑灿和对应项目BA、SE
在公司OA上提交调休或请假申请电子流
发送:对应项目BA、SE邮箱
抄送:wangshun@oppo.com;zhengcan1@oppo.com;lilun@e-lead.cn;
- 严禁将用户信息,数据, 通知等...在未得到用户明确授权时通过任何渠道【微信,QQ,邮件,微博...】转发或外发.
- 严禁将移动设备【U盘,手机,移动硬盘等...】接入用户提供的办公设备.
- 严禁通过第三方仓库【GitHub,DockHub, GitLab...】等上传用户代码.
【未完待续】
| 创建时间: | 2021/12/29 11:35 |
| 更新时间: | 2022/1/3 15:10 |
| 作者: | Chris |

11:23:45 当前系统时间,可以通过data 命令获取up 15 min 获取系统最后一次重启到现在连续运行的时间1 user 当前系统登录用户数, 可以通过who 命令或 w 获取
load average 系统前 1, 5, 15分钟的平均负载率
0.05 表示从当前时间到过去的15分钟,大概有0.05个进程或线程在等待cpu资源,换句话说就是cpu的95%时间是闲置的,没有cpu压力
如果这个值是5表示5倍于cpu处理能力的进程或线程在等待cpu资源,但并不代表cpu在满负荷运行。

grep 'model name' /proc/cpuinfo | wc -l
Tasks: 189 total, 1 running, 188 sleeping, 0 stopped, 0 zombie
现在总共有 189个进程
1个在运行中
188个在休眠中
0个停止状态
0个僵尸进程
僵尸进程是指子进程退出后父进程还在运行,父进程没有获取子进程的退出状态,子进程为僵尸状态, 一般是因为程序问题,重启可以解决
%Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st


KiB Mem : 2027904 total, 1292740 free, 409816 used,
KiB Swap: 2097148 total, 2097148 free, 0 used. 1457132 avail Mem
2027904 total 系统现在共有2027904KiB的内存
1292740 free 系统现在空闲的内存1292740KiB有内存
409816 used 被程序占用的内存有409816KiB
325348 buff/cache 磁盘交换或者缓存占用的空间有325348KiB
2097148 total 系统现在共有2097148KiB的交换分区空间
2097148 free 系统现在空闲的交换分区1292740KiB
1457132 avail Mem 表示可用于进程下一次分配的物理内存1457132KiB


人为对进程的优先级是不能控制的,只能通过执行优先级加权
| 创建时间: | 2021/12/31 15:37 |
| 更新时间: | 2021/12/31 16:02 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/bjxq-cs88/p/9759571.html |
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:
- 在队列为空时,消费线程会等待队列变为非空。
- 当队列满时,生产线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

异常 :是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。返回特殊值 :插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null一直阻塞 :当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。超时退出 :当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内.
没有
ThreadLocal的时候,一个线程在其生命周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而
ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。
不要拿
ThreadLocal和synchronized做类比,因为这种比较压根就是无意义
sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问
而ThreadLocal从本质上讲,无非是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”
为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了

执行结果,可以看到每个线程都分配到了一个唯一的ID,同时在此线程范围内的"任何地点",我们都可以通过ThreadId.get()这种方式直接获取。

set操作,为线程绑定变量

可以看到,ThreadLocal不过是个入口,真正的变量是绑定在线程上的。

下面给是Thread类中的定义,每个线程对象都拥有一个ThreadLocalMap对象
ThreadLocal.ThreadLocalMap threadLocals = null;
现在可以看出ThreadLocal的设计思想了:
- ThreadLocal仅仅是个变量访问的入口;
- 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;
- ThreadLocalMap以当前的threadlocal对象为key,以真正的存储对象为value。get时通过threadlocal实例就可以找到绑定在当前线程上的对象。
乍看上去,这种设计确实有些绕。我们完全可以在设计成
Map<Thread,T>这种形式,一个线程对应一个存储对象。
ThreadLocal这样设计的目的主要有两个:
- 可以保证当前线程结束时相关对象能尽快被回收;
- ThreadLocalMap中的元素会大大减少,我们都知道map过大更容易造成哈希冲突而导致性能变差。
- 还有一个会引起疑惑的问题,我们说ThreadLocal为每一个线程维护一个独立的变量副本,那么是不是说各个线程之间真正的做到对于对象的“完全自治”而不对其他线程的对象产生影响呢?其实这已经不属于对于ThreadLocal的讨论,而是你出于何种目的去使用ThreadLocal。如果我们为一个线程关联的对象是“完全独享”的,也就是每个线程拥有一整套的新的 栈中的对象引用+堆中的对象,那么这种情况下是真正的彻底的“线程独享变量”,相当于一种深度拷贝,每个线程自己玩自己的,对该对象做任何的操作也不会对别的线程有任何影响。
- 另一种更普遍的情况,所谓的独享变量副本,其实也就是每个线程都拥有一个独立的对象引用,而堆中的对象还是线程间共享的,这种情况下,自然还是会涉及到对共享资源的访问操作,依然会有线程不安全的风险。所以说,ThreadLocal无法解决线程安全问题。 所以,需不需要完全独享变量,进行完全隔离,就取决于你的应用场景了。可以想象,对象过大的时候,如果每个线程都有这么一份“深拷贝”,并发又比较大,对于服务器的压力自然是很大的。像web开发中的servlet,servlet是线程不安全的,一请求一线程,多个线程共享一个servlet对象;而早期的CGI设计中,N个请求就对应N个对象,并发量大了之后性能自然就很差。

- ThreadLocal在spring的事务管理,包括Hibernate的session管理等都有出现,在web开发中,有时会用来管理用户会话 HttpSession,web交互中这种典型的一请求一线程的场景似乎比较适合使用ThreadLocal,但是需要特别注意的是,由于此时session与线程关联,而tomcat这些web服务器多会采用线程池机制,也就是说线程是可复用的,所以在每一次进入的时候都需要重新进行set,或者在结束时及时remove。






| 创建时间: | 2020/9/2 14:45 |
| 更新时间: | 2021/12/16 16:45 |
| 作者: | Chris |
影响JVM工作性能的主要是内存,因此对JVM的调优主要是针对内存的调优
为了更好更充分的使用内存,JVM在设计时对内存进行分类,分为:堆内存和栈内存
1. 堆是存储单元,所有需要使用到的数据都在堆中,堆是可以共享的。
2. 栈是运行单元,所有的运行对象都在栈里面,每一个线程都分会被分配栈内存,栈分为私有栈和本地栈,私有栈之间是不共享的,本地栈之间是可以共享的。
3. 堆是用来处理数据的,栈是用来处理逻辑的,这样的好处是可以将数据与逻辑处理分离提交处理效率。
因为对大片内存扫描效率很低,为了更好的进行GC,将堆分为三代
Young Generation
1.1 Eden : 对象首先存在Eden区
1.2 Survivor 0: Eden区满了之后再将对象复制到S0
1.3 Survivor 1: S0满了之后再将对象复制到S1
Old Generation: Survivor区满了之后再将对象复制到Old Generation, Old Generation处理在Young Generation中通过GC回收后还存活的对象
Permaneent Generation , JDK8之后更名为Metaspace [元空间], 存放静态方法和类。
Metaspace:元空间是本地内存里面,主要解决OOM及不方便设置的问题。
回收的是内存,主要对堆中的三代进行回收
回收分为两种:Scavenge GC , Full GC
Scavenge GC 主要针对Young Generation进行回收, 先对Edan区进行回收,如果回收完后还有存活对象,会将对象复制到Survivor区,由于Edan对象变化很频繁,因此Young Generation会经常使用Scavenge GC进行回收。
Full GC 对三代都回进行回收,但由于jdk8之后没有Permanent Generation,取尔代之的是metaspace,所以只能Young Generation和Old Generation进行回收
因为Full GC回收的内容比较多,所以一般情况下不会频繁触发Full GC,只有在如下情况下才会触发
3.1 年老代写满,此时年轻代肯定也写满
3.2 持久代写满
3.3 system.gc()被调用
3.4 上一次GC后,heap分配的策略重新分配了
引用计数
对象的引用进行计数,如果对此对象引用一次,则记数增加1,如是引用释放则减1; 如果对象的记数为0,则表示此对象需要进行回处理。
标记清除
此算法会不断的扫描GC Roots, 将存活的对象标记出来,等到不用的时候再清除, 这个方法效率低,清除完后内存不会连续在一块,形成内存碎片。
复制算法
将内存分为两份,每次只使用其中的一份内存,当回收完成之后,将回收到的内容全部拼接到未使用的那一份内存上面,这样效率高,没有内存碎片
串行回收器
年轻代和年老代都串行,只有一个线程在工作,无需线程交互,所以效率高,但是无法发挥多处理器的优势。
通常在小数据量【100M左右】的多处理器的机器上可以使用串行
使用 -XX:+UseSerialGC 打开串行回收器
并行回收器
在年轻代处理并行回收处理,年老代仍然为串行,会制约回收能力
使用 -XX:+UseParallelGC 打开年轻代并行回收器
并发回收器
在年轻代处理并行回收处理的同时将年老代设置为并行回收
使用 -XX:+UseParallelOldGC 打开年老代并行回收器
使用 -XX:ParallelGCTheads=n 设置并行回收的线程数,此值一般设置为与处理器数相等
修改JVM参数的方式
eclipse
修改tomcat中的文件
2.1 bin/catalina.sh
2.2 bin/startup.sh 服务器停止之后修改的JVM值还是生效的
每个参数用空格隔开,参数区分大小写
第一种语法
set CATALINA_OPTS = -Xmx512m -Xms512m -XX:SurvivorRatio=3 -XX:NewSize=250m -XX:MetaspaceSize=300m
第二种语法
set JAVA_OPTS = -Xmx512m -Xms512m -XX:SurvivorRatio=3 -XX:NewSize=250m -XX:MetaspaceSize=300m
第三种语法
JAVA_OPTS = "-Xmx512m -Xms512m -XX:SurvivorRatio=3 -XX:NewSize=250m -XX:MetaspaceSize=300m"
例:
JAVA_OPTS= "-Xms2048M -Xmx2048M -XX:+PrintGCDetails
-XX:+PrintGCDateStamps -Xloggc:${BAYMAX_APP_WORKDIR}/logs/gc-%t.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=14
-XX:GCLogFileSize=100M -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=${BAYMAX_APP_WORKDIR}/logs/"
-Xms: 初始堆大小
-Xmx: 最大堆大小
初始堆大小一般设置与最在堆大小一至
-XX:NewSize=n 设置年轻代大小
-XX:MaxNewSize=n 设置年轻代最大空间大小
-XX:NewRatio=n 设置年轻代与年老代的比例,如果为3,年轻代:年老代=1:3 , 表示年轻代占年轻代和年老代总和的1/4
-XX:SurvivorRatio=n 设置年轻代中Eden区与两个survivor区的比例,eden:survivor=3:2, 一个survivor占整个年轻代的1/5
-XX:MaxPermSize=n 设置持久代大小
-Xss:设置每个线程的堆大小
总内存大小:
Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]

-XX:MetasapceSize 初始元空间大小,内存使用达到该值时就会触发垃圾收集,同时GC会对该值进行调整,如果释放大量的空间就适当下调该值,如果释放很少的空间则在不超过MaxMetaspaceSize前提下,上调该值。
-XX:MaxMetaspaceSize 最大元空间大小,默认没有限制
-XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾回收
-XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾回收
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParallelOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器
ps aux | grep tomcat
| 创建时间: | 2021/11/23 17:49 |
| 更新时间: | 2021/11/23 18:00 |
| 作者: | Chris |
Alibaba开源的Java诊断工具
当遇到如下问题时,Arthas可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从JVM内查找某个类的实例?
创建目录
C:\chris\arthas
进入此目录下载文件
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
在运行java -jar arthas-boot.jar之前需要确保内存中有一个java进程,idea,mvn,tomcat都是一个java进程,否则会报找不到java进程错误
第一次下载时遇到如下问题,但是第二次再试就Ok了
$ java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.4
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 10228 org.jetbrains.idea.maven.server.RemoteMavenServer36
[2]: 31736 org.jetbrains.jps.cmdline.Launcher
[3]: 30620
3
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/3.5.4?mirror=aliyun
[INFO] File size: 12.72 MB, downloaded size: 4.17 MB, downloading ...
[INFO] File size: 12.72 MB, downloaded size: 10.49 MB, downloading ...
[INFO] Download arthas success.
[INFO] arthas home: C:\Users\13622314539\.arthas\lib\3.5.4\arthas
[INFO] Try to attach process 30620
[WARN] Current VM java version: 1.8 do not match target VM java version: 11, attach may fail.
[WARN] Target VM JAVA_HOME is C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\jbr, arthas-boot JAVA_HOME is C:\Program Files\Java\jdk1.8.0_131\jre, try to set the same JAVA_HOME.
[ERROR] Start arthas failed, exception stack trace:
java.io.IOException: Non-numeric value found - int expected
at sun.tools.attach.HotSpotVirtualMachine.readInt(HotSpotVirtualMachine.java:299)
at sun.tools.attach.HotSpotVirtualMachine.loadAgentLibrary(HotSpotVirtualMachine.java:63)
at sun.tools.attach.HotSpotVirtualMachine.loadAgentLibrary(HotSpotVirtualMachine.java:79)
at sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:103)
at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:120)
at com.taobao.arthas.core.Arthas.<init>(Arthas.java:26)
at com.taobao.arthas.core.Arthas.main(Arthas.java:139)
[ERROR] attach fail, targetPid: 30620
$ java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.4
[INFO] Process 30620 already using port 3658
[INFO] Process 30620 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 30620
[2]: 10228 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 31736 org.jetbrains.jps.cmdline.Launcher
1
[INFO] arthas home: C:\Users\13622314539\.arthas\lib\3.5.4\arthas
[INFO] The target process already listen port 3658, skip attach.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.4
main_class
pid 30620
time 2021-11-03 15:05:15
可以看到arthas安装包下载到了目录
arthas home: C:\Users\13622314539\.arthas\lib\3.5.4\arthas
在C:\Users\13622314539下有两个目录和arthas有关
如果启动时端口号被占用,可以用如下命令更换端口号
$ java -jar arthas-boot.jar --telnet-port 9090 --http-port 8090
启动后arthas支持游览器查看
http://localhost:3658/
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
在运行java -jar arthas-boot.jar之前需要确保内存中有一个java进程,idea,mvn,tomcat都是一个java进程,否则会报找不到java进程错误
在家目录下可以看到与arthas相关的两个目录
cd ~
ls -a
- .arthas
- logs
https://arthas.aliyun.com/doc/en/download.html#download-from-maven-central
在C:\Users\13622314539下有两个目录和arthas有关
直接手动删除就可以完成在windows下的卸载
rm -rf ~/.arthas
rm -rf ~/logs
进行目录,启动需要被诊断的进程
C:\Users\13622314539\.arthas\lib\3.5.4\arthas
启动math-game.jar
java -jar math-game.jar
选择需要被诊断的进程math-game,会自动attach成功
13622314539@OC136223145391 MINGW64 /c/chris/arthas
$ java -jar arthas-boot.jar --telnet-port 9090 --http-port 8090
[INFO] arthas-boot version: 3.5.4
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 33616 math-game.jar
[2]: 10228 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 31736 org.jetbrains.jps.cmdline.Launcher
[4]: 30620
1
[INFO] arthas home: C:\Users\13622314539\.arthas\lib\3.5.4\arthas
[INFO] Try to attach process 33616
[INFO] Attach process 33616 success.
[INFO] arthas-client connect 127.0.0.1 9090
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.4
main_class
pid 33616
time 2021-11-03 16:14:36
[arthas@33616]$
第一部分显示当前进程中运行的所有线程
第二部分显示JVM的内存使用情况
第三部分显示操作系统信息和JAVA版本号

[arthas@18824]$ thread
[arthas@30620]$ thread 1
"main" Id=1 RUNNABLE
[arthas@33616]$ jad demo.MathGame
jad demo.MathGame
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@42a57993 -- 程序加载器
+-sun.misc.Launcher$ExtClassLoader@544bdaf7 -- 扩展加载器
Location: -- 定位到相应的jar包
/C:/Users/13622314539/.arthas/lib/3.5.4/arthas/math-game.jar
-- 反编译原代码
/*
* Decompiled with CFR.
*/
package demo;
通过watch命令可以查看方法的入参和返回值分别是什么
watch package.class method returnObj
[arthas@18824]$ watch demo.MathGame primeFactors returnObj
method=demo.MathGame.primeFactors location=AtExit
ts=2021-11-11 14:50:00; [cost=0.0738ms] result=@ArrayList[
@Integer[2],
@Integer[5],
@Integer[5],
@Integer[2287],
]
help 查看命令帮助信息
[arthas@18824]$ help
NAME DESCRIPTION
help Display Arthas Help
auth Authenticates the current session
keymap Display all the available keymap for the specified connection.
打印某个文件的内容, 如果没有写路径则显示当前目录下的文件
在C:\Users\13622314539\.arthas\lib\3.5.4\arthas目录下新增文件test.txt
[arthas@18824]$ cat test.txt
cat test.txt
dddd
[arthas@18824]$
匹配查找
| 参数列表 | 作用 |
|---|---|
| -n | 显示行号 |
| -i | 忽略大小写 |
| -m | 最大显示行数,要与查询字符串一起使用 |
| -e | 使用正则表达式 |
只显示包含java字符串的系统属性
[arthas@18824]$ sysprop | grep java
显示包含java字符串的行和行号的系统属性
[arthas@18824]$ sysprop | grep -n java
显示包含system字符串的前10行系统属性
[arthas@18824]$ sysprop | grep -m 10 java
打印当前工作目录路径
清屏
查看当前会话的信息, 两个会话可以同时监听同一个PID
[arthas@21904]$ session
session
Name Value
--------------------------------------------------
JAVA_PID 21904
SESSION_ID c54d94b2-f155-4db1-8a51-8ab7dd09a03a
[arthas@21904]$
重置增强类,将被arthas增强过的类全部还原,Arthas服务端关闭时会重置所有增强过的类
输出当前java进程所加载的arthas的版本号
[arthas@21904]$ version
version
3.5.4
输出arthas的命令历史
[arthas@21904]$ history
history
1 dashboard
2 cls
3 thread
4 cls
5 thead
6 thread
7 thread 1
8 jad demo.MathGame
9 jad demo.MathGame
10 jad demo.MathGame
11 jad demo.MathGame
12 dashboard
输出arthas快捷键列表及自定义快捷键
[arthas@21904]$ keymap
keymap
Shortcut Description Name
-------------------------------------------------------------------------------
"\C-a" Ctrl + a beginning-of-line
"\C-e" Ctrl + e end-of-line
"\C-f" Ctrl + f forward-word
"\C-b" Ctrl + b backward-word
"\e[D" Left arrow backward-char
"\e[C" Right arrow forward-char
"\e[A" Up arrow history-search-backward
"\e[B" Down arrow history-search-forward
"\C-h" Ctrl + h backward-delete-char
[arthas@21904]$ trace demo.MathGame print
trace demo.MathGame print
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 301 ms, listenerId: 1
`---ts=2021-11-23 16:25:27;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@42a57993
`---[0.7906ms] demo.MathGame:print()
`---ts=2021-11-23 16:25:28;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@42a57993
`---[0.1149ms] demo.MathGame:print()
| 创建时间: | 2021/11/4 14:32 |
| 更新时间: | 2021/11/4 14:32 |
| 作者: | Chris |
| 创建时间: | 2020/11/24 12:51 |
| 更新时间: | 2021/6/4 10:15 |
| 作者: | Chris |
| 来源: | https://www.120ask.com/question/67330848.htm |


| 创建时间: | 2021/4/7 10:02 |
| 更新时间: | 2021/4/7 10:04 |
| 作者: | Chris |
WebMvcConfigurer之HttpMessageConverter
在SpringMVC中,可以使用@RequestBody和@ResponseBody两个注解,分别完成请求报文到对象和对象到响应报文的转换
底层这种灵活的消息转换机制就是利用
HttpMessageConverter来实现的,Spring内置了很多HttpMessageConverter,比如
MappingJackson2HttpMessageConverter,StringHttpMessageConverter等
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
通过实现
WebMvcConfigurer接口来配置FastJsonHttpMessageConverterSpringboot2.0版本以后推荐使用这种方式来进行web配置,这样不会覆盖掉springboot的一些默认配置。
配置类如下
/**
* 消息内容转换配置
* 配置fastJson返回json转换
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//调用父类的配置
super.configureMessageConverters(converters);
//创建fastJson消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//创建配置类
FastJsonConfig fastJsonConfig = new FastJsonConfig();
//修改配置返回内容的过滤
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullStringAsEmpty
);
fastConverter.setFastJsonConfig(fastJsonConfig);
//将fastjson添加到视图消息转换器列表内
converters.add(fastConverter);
}
fastJson配置实体调用setSerializerFeatures方法可以配置多个过滤方式,常用的如下:
1、WriteNullListAsEmpty :List字段如果为null,输出为[],而非null
2、WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null
3、DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)
4、WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null
5、WriteMapNullValue:是否输出值为null的字段,默认为false。
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.clear();
stringConverter();
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
//不显示为null的字段
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
String fieldName = gen.getOutputContext().getCurrentName();
try {
//反射获取字段类型
Field field = gen.getCurrentValue().getClass().getDeclaredField(fieldName);
if (Objects.equals(field.getType(), String.class)) {
//字符串型空值""
gen.writeString("");
return;
} else if (Objects.equals(field.getType(), List.class)) {
//列表型空值返回[]
gen.writeStartArray();
gen.writeEndArray();
return;
} else if (Objects.equals(field.getType(), Map.class)) {
//map型空值返回{}
gen.writeStartObject();
gen.writeEndObject();
return;
}
} catch (NoSuchFieldException e) {
}
//默认返回""
gen.writeString("");
}
});
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS );
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
//放到第一个
converters.add(0, jackson2HttpMessageConverter);
}
| 创建时间: | 2020/9/2 15:33 |
| 更新时间: | 2021/2/9 15:59 |
| 作者: | Chris |
| 来源: | https://www.processon.com/diagraming/5f8143f21e085307a080a11f |
- 慢查询日志, 把有问题的SQL记录下来
- explain执行计划,会看执行计划
- show profile,查看SQL执行时的硬件消耗
在执行查询时一般只会用到一个索引,由优化器去选择。
解析过程 www.cnblogs.com/annsshadow/p/5037667.html

1.客户端连接数据库,验证身份。
2.获取当前用户权限。
3.当你查询时,会先去缓存看看,如果有返回。
4.如果没有,分析器对sql做词法分析和语义分析。
5.优化器对sql进行“它认为比较好的优化”。
6.执行器负责具体执行sql语句。
7.最后把数据返回给客户端。
from
on
join
where
group by
having
select
order by
3层Btree可以存放上百万条数据
Btree一般指B+树,数据都存放在叶节点里面
show index from table_name;
mysql 的优化器分干扰我们的优化
分析SQL的执行计划, explain, 可以模拟优化器执行。
id 相同,从上向下依次执行。
表的执行顺序因表中数据量的改变而改变,原因是因为笛卡尔积
多表关联时,结果记录数没有变,但是中间结果不同, 中间结果小,占用内存小,所以数据少的表作为驱动表优先查询。


id 值不相同时,值越大越优先执行。因为嵌套子查询时先查内层再查外层。
嵌套查询时先查内层,再查外层

| 类型 | 描述 |
|---|---|
| primary | 包含子查询SQL的主查询语句一般为最外层 |
| SUBQUERY | 包含子查询SQL的子查询语句一般为非最外层 |
| SIMPLE | 简单查询,不包含子查询和union连接查询 |
| DERIVED | 衍生查询,在查询时用到了临时表 |
| a.在子查询的from后面只有一张表 | |
| b.在子查询from后面有两张表, 如果table 1 union table2, 则table1就是derived,table2就是union |
数字2表示衍生查询使用了id为2的表

system > const > eq_ref > ref > range > index > all, 越往左越高效
其中 system,const 只是理想情况,通过自己优化实际能达到 ref > range
只有一条数据的系统表,或衍生表只有一条数据的主查询

必须通过primarykey 或 unique索引查询时,并且仅仅能查到一条数据。

唯一性索引
对于每一个索件键的查询返回值匹配唯一一行数据,有且只有一个值,不能多也不能是0,查询的结果和表中实际的条数是一样的,常见于primary key或unique索引查询时。
比如通过名字查询时,每个名字只有一个值,不能出现两个John
alter table taechercard add constraint pk_tcid primary key(tcid);
alter table taecher add constraint uk_tcid unique index(tcid);
select t.tcid from teacher t , teachercard tc where t.tcid=tc.tcid;
通过外键tcid查询时,teacher里面的记录数要和teachercard里面的记录数一致,并全tcid在teacher里面作为外键是唯一值。

非唯一性索引, 对于每一个索件键的查询返回值匹配一行,0行或多行数据。
alter table teacher add index index_name(tname);


范围索引
检索指定范围的行, where后面是一个范围查询, between, in > < >= <=, in有时走range有时会是all.
将索引树全部查询了一遍
也就是将索引列的所有值全部查询了一遍
alter table teacher add index tid_index(tid);

在tid上没有建立索引,所以需要将全表扫描

possible_keys 可能用到的索引,是一种预测,但是不准确
key 实际用到的索引
如果对应的值为null说明没有索引
用于判断复合索引是否被完全使用
alter table teacher add index index_name(tid);
create table test_kl(
name char(20) not null default ''
);
alter table test_kl add index index_name(name);
select * from test_kl where name='';
uft8 一个字符3个字节
gbk 一个字符2个字节
latin 一个字符1个字节
20 x 3 = 60

mysql对于为空的牵引列额外用1个字节标识
alter table add column name1 char(20);
alter table test_k1 add index index_name1(name1);
20 x 3 +1= 61

指明当前表参照的字段:
select * from where a.c=b.x; (其中b.x可以是常量, const)


using filesort:
性能消耗大,需要额外的一次排序(查询),常见于排序中
1 对于单牵引如果查找和排序不是同一个字段就会出现using filesort
如果查找和排序是同一个字段则不会出现using filesort
可以按照查找什么字段就用什么字段进行排序
select * from test02 where a1= '' order by a1; --不会出现 using filesort
select * from test02 where a1= '' order by a2; -- using filesort
1.2 复合索引,最佳左前缀,不能跨列.
alter table test02 add index idx_a1_a2_a3
explain select * from test02 where a1='' order by a3 -- using filesort
explain select * from test02 where a2='' order by a3 -- using filesort
explain select * from test02 where a1='' order by a2 --不会出现 using filesort
using temporary:
性能损耗比较大,用到了临时表,常见于group by 中.
如果查找和分组不是同一个字段则会出现using temporary
所以如果查找哪些队就用这些列进行分组
select * from test02 where a1 in ('1','2','3') group by a1; -- 不会出现using temporary
select * from test02 where a1 in ('1','2','3') group by a2; -- using temporary
using index
意味着性能提升,因为所有的查询列都在索引中即【索引覆盖】
原因是不需要读取原文件,只从索引文件获取数据, 即没有回表查询
alter table test01 add index a1_a2_a3_inx (a1,a2,a3);
select a1, a2, a3 from test01 where a1='' --using index
drop index a1_a2_a3_inx on table test01 ;
alter table test01 add index a1_a2_inx (a1,a2);
select a1, a3 from test01 where a1='' and a3='' -- 不用使用using index
using where
需要回表查询,即需要从索引查又需要回原表查,
alter table test01 add index age_inx(age);
select name , age from test01 where age='' 此语句必须回原表查name,因此会显示using where
impossible where
where 永远为false.
select * from test02 where a1='x' and a1='y' -- impossible where

| 创建时间: | 2020/9/2 14:35 |
| 更新时间: | 2020/9/2 14:39 |
| 作者: | Chris |
Kafka: LinkedIn开源的分布式发布-订阅消息系统
解耦
需求是变化的,应对变化的需要冗余
机器故障,避免数据丢失扩展性
消息队列解耦了处理过程,增大消息入队和处理的能力变得容易灵活性与峰值处理能力
消息队列能够使关键组件顶住突发的访问压力而不会因为突发的超负荷的请求而完全崩溃
高吞吐率、低延迟
• 每秒处理几十万条消息,延迟最低几毫秒
可扩展性
• 支持动态扩展节点数据
持久性与可靠性
• 数据被持久化到磁盘上,支持数据多副本防止数据丢失
高容错
• 允许节点失败
高并发
• 支持上千个客户端同时读写
消息在一个partition中是有序的,但大于两个partition则存储时变成了无序将消息设置为同一个Key,这样就到存到同一个partition中

producer将数据写入到kafka的memstore, 当存满时会刷到本地磁盘中去当消费时会先去memstore查数据如果没有则去本地磁盘中检索。
向broker发送消息, 可通过任意一个broker发现其他broker的位置信息,因为所有broker的信息在zoopkeeper中注册
消息组成
topic+key+value+timestamp
- Producer 和 consumer之间的桥梁从producer端接收消息,并保存下来将消息发送给订阅的consumer
- 可将消息可靠地缓存一段时间每个消息保存成多副本(默认是3)可设置保存时间(默认一周)
用户划分message的逻辑概念,一个topic可以分布到不同broker上
Kafka横向扩展和一切并行化的基础,每个topic至少被切分成1个partition消息在Partition中是有编号的,称为“offset”
Kafka以Partition为单位对消息进行备份(replica),每个partition可以配置至少有1个replica
用户应用程序,负责从kafka中读取数据,并进行处理;
多个consumer可形成一个group,同时读取某个topic;
每个consumer读取一个或多个partition
每个consumer自己维护读取的位置(offset,一旦挂掉后,重启可继续读取)
Message 被追加到append-only文件中
Producer向文件中追加消息(顺序写)
Consumer从文件中读取一定范围的消息 (顺序读)

KafkaProducer -> 通过加载配置文件生成Producer Bean producer
ProducerRecord -> 封装了 topic, key, value, partition 和 timestamp
RecordMetadata -> Broker确认收到一条消息后,应答的元信息,包括
offset,checksum和partition等信息
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<Integer, String> producer = newKafkaProducer<>(props)
bootstrap.servers -> 初始化Broker列表
key.serializer/value.serializer -> Key和value序列化器
acks -> 可靠性级别:可选值:0,1(default),all
buffer.memory -> Producer数据缓存大小(等到满后会发给broker)
compression.type -> 数据压缩方式:none, gzip, snappy, 或lz4.
max.request.size/batch.size -> 每个batch数据量
producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), Integer.toString(i)));
http://kafka.apache.org/0100/documentation.html#producerconfigs
KafkaConsumer -> 与Kafka broker通信的类,常用方法是subscribe和poll
ConsumerRecord -> 一个返回结果,包含topic,offset,key/value,timestamp等信息
ConsumerRecords -> 一组返回结果,保存每个partition对应的结果集
ConsumerConfig -> 参数配置,可通过该对象设置程序参数
Properties props = new Properties();
props.put(“bootstrap.servers”, “test1,test2,test3”);
props.put("groupid", “123”);
props.put("enable.auto.commit", ”true");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"
consumer = new KafkaConsumer<>(props);
bootstrap.servers -> 初始化Broker列表
key.deserializer/value.deserializer -> Key和value反序列化器
fetch.min.bytes -> 每次请求至少返回的数据大小(默认是1字节)
group.id -> Consumer所在的group
session.timeout.ms -> Session超时时间,一旦超时,该consuemer将被移除所在group
enable.auto.commit -> 是否自动提交offset
// subscribe topics
consumer.subscribe(Arrays.asList(topic));
// read data from subscribed topics
ConsumerRecords<String, String> records = consumer.poll(10000);
http://kafka.apache.org/0100/documentation.html#consumerconfigs
如果某个topic的分区数小于接收线程数,则部分线程空闲
如果topic的分区数大于接收线程数,则部分接收线程会同时读取多个分区中的数据
同一个线程收到的数据可能来自多个Partition,不保证数据的顺序性
bin/zookeeper-server-start.sh config/zookeeper.properties &
bin/kafka-server-start.sh -daemon config/server.properties
jps | grep Kafka
如果启动失败,可以查看kafka安装目录下的logs/kafkaServer.out文件,寻找失败原因
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 5 --topic test --config delete.retention.ms=172800000
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
为什么说Partition是Kafka横向扩展和一切并行化的基础,每个topic至少被切分成1个partition ?
| 创建时间: | 2020/9/2 14:11 |
| 更新时间: | 2020/9/2 14:31 |
| 作者: | Chris |
https://www.jianshu.com/p/2838890f3284
https://www.cnblogs.com/520playboy/p/6750023.html
https://www.cnblogs.com/SimpleWu/p/12112351.html
https://www.cnblogs.com/xiaobaoTribe/p/11381675.html
https://www.jianshu.com/p/5b0cde62b3fc
https://www.jianshu.com/p/345aaa18f71d
https://blog.csdn.net/itanping/article/details/100919270
| 创建时间: | 2020/11/3 9:19 |
| 更新时间: | 2020/11/3 9:19 |
| 作者: | Chris |
http://easypoi.mydoc.io/
| 创建时间: | 2020/10/23 12:51 |
| 更新时间: | 2020/10/24 6:57 |
| 作者: | Chris |
| 来源: | https://www.cnblogs.com/zhi-leaf/p/10979629.html |
https://nodejs.org/en/download/
cd /usr/local/
scp node-v12.19.0-linux-x64.tar.xz root@master:/usr/local
tar -xvf node-v12.19.0-linux-x64.tar.xz // 解压
mv node-v12.19.0-linux-x64 nodejs
ln -s /usr/local/nodejs/bin/npm /usr/local/bin/
ln -s /usr/local/nodejs/bin/node /usr/local/bin/
node -v
npm -v

https://nodejs.org/en/download/

默认安装,点击下一步
Win + R , 输入cmd ,打开dos命令行,输入 node -v 查看NodeJS版本号
环境配置主要配置的是npm安装的全局模块所在的路径,以及缓存cache的路径.
之所以要配置,是因为以后在执行类似:npm install express [-g] (后面的可选参数-g,g代表global全局安装的意思)的安装语句时,会将安装的模块安装到【C:\Users\用户名\AppData\Roaming\npm】路径中,占C盘空间
node_global
node_cache

在当前文件夹,长按Shift + 鼠标右键 打开cmd命令行

npm config set prefix "F:\Program Files\nodejs\node_global"
npm config set cache "F:\Program Files\nodejs\node_cache"
在【系统变量】下新建【NODE_PATH】
NODE_PATH
F:\Program Files\nodejs\node_global\node_modules
将【用户变量】下的【Path】追加
Path
F:\Program Files\nodejs\node_global
npm install express -g # -g是全局安装的意思
npm install express -g

| 创建时间: | 2020/9/2 22:06 |
| 更新时间: | 2020/10/23 11:44 |
| 作者: | Chris |
root@master:/usr/local/zookeeper# wget https://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz
tar -zxvf apache-zookeeper-3.6.2-bin.tar.gz
mv apache-zookeeper-3.6.2-bin zookeeper
cd zookeeper/conf
更改默认配置文件名称
mv zoo_sample.cfg zoo.cfg
编辑配置文件,自定义dataDir
vi zoo.cfg
dataDir=/usr/local/zookeeper/data
cd /bin
./zkServer.sh start
./zkServer.sh stop

使用 zkCli.sh 连接测试
root@master:/usr/local/zookeeper/bin# ./zkCli.sh -server 127.0.0.1:2181
[zk: 127.0.0.1:2181(CONNECTED) 7] ls /
[zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 8] ls /zookeeper
[config, quota]
| 创建时间: | 2020/10/14 23:38 |
| 更新时间: | 2020/10/14 23:50 |
| 作者: | Chris |
当一个对象被创建后,有一个以上的引用变量引用它。那么它就出于可达状态,程序可以通过引用变量来调用该对象的属性和方法。
如果程序中某个对象不再有任何引用变量引用它,他将进入可恢复状态,
在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存。在回收该对象之前,系统会调用可恢复状态的对象的finalize方法进行资源清理,如果系统调用finalize方法重新让一个以上的引用变量引用该对象,则该对象会再次编程可达状态;否则,该对象将进入不可达状态。
当对象的所有关联都被切断,且系统调用所有对象的finalize方法依然没有使该对象变成可达状态后,这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象出于不可达状态,系统才会真正回收该对象所占用的资源。

调用 System 类的 gc() 静态方法:System.gc()
调用 Runtime 对象的 gc() 实例方法:Runtime.getRuntime().gc()
这种强制只是建议系统立即进行垃圾回收,系统完全有可能建议完全置之不理,垃圾回收机制会在收到通知后,尽快进行垃圾回收。
永远不要主动调用某个对象的finalize 方法
该方法应交给垃圾回收机制调用。finalize 方法何时被调用,是否被调用具有不确定性,不要把finalize 方法当成一定会被执行的方法。
在JVM的规范中,只规定了JVM必须要有垃圾回收机制,但是什么时候回收却没有明确说明。也就是说,对象成为了垃圾对象之后, 并不一定会马上就被垃圾回收。
由于 Sun 公司的 JVM 采用的是“最少”回收的机制,因此不应当把释放资源的代码写在 finalize 方法中。
| 创建时间: | 2020/10/14 23:34 |
| 更新时间: | 2020/10/14 23:38 |
| 作者: | Chris |
被强引用指向的对象,绝对不会被垃圾收集器回收。
Integer prime = 1; 这个语句中prime对象就有一个强引用。
被SoftReference指向的对象可能会被垃圾收集器回收,但是只有在JVM内存不够的情况下才会回收;
如下代码可以创建一个软引用:
Integer prime = 1;
SoftReference<Integer> soft = new SoftReference<Integer>(prime);
prime = null;
当一个对象仅仅被WeakReference引用时,在下个垃圾收集周期时候该对象就会被回收。
我们通过下面代码创建一个WeakReference:
Integer prime = 1;
WeakReference<Integer> soft = new WeakReference<Integer>(prime);
prime = null;
当把prime赋值为null的时候,原prime对象会在下一个垃圾收集周期中被回收,因为已经没有强引用指向它。
| 创建时间: | 2020/10/14 23:29 |
| 更新时间: | 2020/10/14 23:33 |
| 作者: | Chris |
Comparable 是排序接口。
若一个类实现了Comparable接口,就意味着“该类支持排序”。此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
接口中通过 x.compareTo(y) 来比较x和y的大小。若返回负数,意味着x比y小;返回零,意味着x等于y;返回正数,意味着x大于y。
Comparator 是比较器接口。
我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。
int compare(T o1, T o2) 和上面的 x.compareTo(y) 类似,定义排序规则后返回正数,零和负数分别代表大于,等于和小于。
https://blog.csdn.net/u010859650/article/details/85009595
| 创建时间: | 2020/9/2 15:02 |
| 更新时间: | 2020/10/13 11:23 |
| 作者: | Chris |
bin: 启停脚本
conf: 配置文件 server.xml和web.xml
lib: 运行相关的jar文件
logs: 日志文件
temp: 临时文件
webapps: 要发布的程序文件
work: 编译后产生的class文件
doc: 存在tomcat文档

客户端向服务器server发请求
连接器connector,默认端口号为8080,连接器为两种一种是http,一种是ajp
连接器请求发给engine
将访问的资源解释到虚拟主机host
匹配到需要的虚拟主机host
通过context获取请求,并通过mapping table 找到对应的servlet
servlet封装request和response,并将结果返回给engine
engine再将结果返回给connector
connector再将结果返回到客户端
注: **servlet 是以servlet编码方式写的一个tomcat服务器端
tomcat是一个web服务器只能处理静态内容,所以需要通过servlet **的方式进行请求处理
mapping table 在tomcat中的./conf/web.xml
目的是为了配置登录监控界面的用户
文件目录: ./conf/tomcat-users.xml
配置的用户名可以随便起名,但是角色
admin-gui: 是用来访问Server Status
manager-gui:是用来访问Host Manager
<user username="test" password="test" roles="admin-gui, manager-gui"/>

/usr/local/apache-tomcat-8.5.31/bin/shutdown.sh
/usr/local/apache-tomcat-8.5.31/bin/startup.sh
1. http://ip:8080
2. http://ip:8080/manager/status
3. http://ip:8080/host-manager/html
如果进入监控页面提示403,表示没有权限去访问服务器资源,需要修改如下文件的内容
1. /usr/local/apache-tomcat-8.5.31/webapps/manager/META-INF/context.xml
2. /usr/local/apache-tomcat-8.5.31/webapps/host-manager/META-INF/context.xml
将allow修改为对应的网段也可以全部修改为 *

https://github.com/psi-probe/psi-probe/releases
下载probe.war包,放到\apache-tomcat-8.5.47\webapps下

使用probe监控工具需要使用权限为manager-gui
文件目录: ./conf/tomcat-users.xml
<role rolename="manager-gui"/>
<user username="test" password="test" roles="manager-gui"/>
http://localhost:8080/probe
tomcat里面有两种连接器,HTTP 和 AJP
<Connector port="8080" protocol="HTTP/1.1" --连接的协议及版本号,表示只能处理此协议的请求
connectionTimeout="20000" --连接超时时间,单位ms
maxThreads="300" --最大线程数,表示同时可以并发处理的线程数,一个http请求对应一个线程
accountCount="150" --最大排队数,若并发的请求数超过maxThreads,就需要排队,超过排队数的请求就被kill掉
redirectPort="8443" --重置端口号,http重置为https后端口号就变成了8443
URIEncoding="UTF-8"
/>
/usr/local/tomcat/conf/logging.properties
#日志文件被记录下来的级别
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
#日志文件保存的目录
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
#日志文件的前缀
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
从高到低分别为:
SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST
1catalina.org.apache.juli.AsyncFileHandler.level = OFF --表示不记录日志
1catalina.org.apache.juli.AsyncFileHandler.level = ON
192.168.140.1 - test [04/Aug/2020:09:06:06 +0000] "GET /probe/js/Tooltip.js HTTP/1.1" 200 20051
192.168.140.1 --访问tomcat的客户端IP
test -- 用户名
+0000 -- 时区偏移
200 -- 最后结果的状态码
20051 -- 最后处理的字节数
catalina.out 与 catalina.2020-08-04.log 类似
主要记录tomcat服务器启停相关的线程信息
localhost.2020-08-04.log
主要记录线程运行时调用的方法和属性以及JAVA线程号
| 创建时间: | 2020/10/7 17:31 |
| 更新时间: | 2020/10/8 14:55 |
| 作者: | Chris |
一个高性能的http和反向代理服务器软件,特点:
用内存少,并发量高.
专为性能优化而开发,能经受高负载,可以支持高达50000个并发连接数。
Nginx支持热部署。
它的启动特别容易, 并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动。
反向代理
负载均衡
动静分离
局域网中的客户端要访问internet,则需要能过代理服务器来访问,一般在客户端【浏览器】中配置代理服务器,这种代理服务就称为正向代理。

客户端不需要做任何配置,对代理无感知,只需要将请求发送给反射代理服务器,再由返现代理服务器去生选择目标服务器获取数据后返回给客户端。
此时代理服务器和目标服务器对外是同一个服务器,暴露的是代理服务器的地址,隐藏的是真实服务器的地址。

将请求平均分配到多个服务器上从而达到系统的可高用
常见的负载均衡组件有,Nginx, LVS, 硬件F5等

为了加快网站的解析速度,可以把动态资源和静态资源由不同的服务器来解析,加快解析的速度,降低单个服务器的压力。
将动态请求和静态请求分开,不能单纯的理解为将动态页面和静态页面分开,可以使用Nginx处理静态页面,Tomcat处理动态页面。

将静态资源独立成单独的域名,存放在独立的服务器上,这是目前的主流方式。
将动态文件和静态文件混合存放在一起,通过Nginx将资源分开。
通过location指定不同的后缀名实现不同的请求转发
通过expire设置可以使浏览器缓存过期时间,减少与服务器之间的请求和流量,这种做法适合静态资源
给一个静态资源设置一个过期时间,例如设置为3d,表示3天内请求同一资源,比对服务器该文件最后更新时间是否发生变化,如果没有发生变化,则不会从服务器获取,并返回状态码304,如果资源发生变化,则从服务器获取最新文件并返回状态码200.
expires 30s; #缓存30秒
expires 30m; #缓存30分钟
expires 2h; #缓存2小时
expires 30d; #缓存30天
查看是否已安装gcc
gcc -v
安装
yum -y install gcc
pcre是一个perl库,包括perl兼容的正则表达式库,Nginx的http模块使用pcre来解析正则表达式,所以需要安装pcre库。
yum install -y pcre pcre-devel
pcre-config --version
zlib库提供了很多种压缩和解压缩方式Nginx使用zlib对http包的内容进行gzip,所以需要安装
yum install -y zlib zlib-devel
yum install -y openssl openssl-devel
http://nginx.org/
cd /opt/
curl -OL http://nginx.org/download/nginx-1.19.3.tar.gz
tar -zxvf nginx-1.19.3.tar.gz
mv nginx-1.19.3 nginx
cd nginx
###运行下面三个命令
./configure
make
make install
也可以同时进行 make && make install
cd /usr/local/nginx/conf
cp nginx.conf nginx.conf.bak
vi nginx.conf
修改为你需要的端口号
server {
listen 1111;
server_name localhost;
cd /usr/local/nginx/sbin
./nginx
验证nginx配置文件是否正确
[root@master sbin]# ./nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
http://master:1111/

使用Nginx的命令时必须先进入到目录/usr/local/nginx/sbin
启动
./nginx
查看版本号
./nginx -v
停用
./nginx -s stop
重加载配置文件
./nginx -s reload
cd /usr/local/nginx/conf
vi nginx.conf
从配置文件开始到events块之间的内容,主要设置一些影响Nginx服务器整体运行的配置指令
worker_processes 1; 值越大,表示处理并发量越大,但是需要考虑硬件和软件及网络因素的制约
主要设置一些影响Nginx服务器与用户网络连接的配置指令
worker_connections 1024; 支持用户的连接数
全局块
配置的指令如文件引入,MIME-TYPE, 日志定义, 连接超时时间,单连接请求数上限等
server块
虚拟机相关的配置,每个http块可以配置多个server块,每个server块相当于一个虚拟主机
每个server块可以包含多个location块
通过在windows中访问 www.123.com:1111 访问vmware中linux中的tomcat

配置window中的host文件
######## Nginx ###########
192.168.101.127 www.123.com
配置nginx.conf文件
server {
listen 1111;
server_name 192.168.101.127;
location / {
# root html;
# index index.html index.htm;
# proxy_pass http://cluster;
proxy_pass http://127.0.0.1:8080;
}
测试
http://www.123.com:1111/

如果是www.123.com:9001/edu/a.html 防问vmware中linux中的tomcat8080
如果是www.123.com:9001/vod/a.html 防问vmware中linux中的tomcat8081
准备两个tomcat
tomcat 8080
tomcat 8081

在tomcat 8080 的/opt/tomcat8081/webapps/ 下新增edu/ 目录, 并在此目录中新增文件a.html, 文件内容如下:
<h4>tomcat 8080!</h4>
在tomcat 8080 的/opt/tomcat8081/webapps/ 下新增vod/ 目录, 并在此目录中新增文件a.html, 文件内容如下:
<h4>tomcat 8081!</h4>
注: 最好重新启动tomcat, 尤其是8080
配置nginx.conf 新增server节点
server {
listen 9001;
server_name 192.128.101.127;
location ~ /edu/ {
proxy_pass http://127.0.0.1:8080;
}
location ~ /vod/ {
proxy_pass http://127.0.0.1:8081;
}
}
启动加载nginx.conf
cd /opt/nginx/sbin
./nginx -s reload
测试
www.123.com:9001/edu/a.html 访问vmware中linux中的tomcat8080
www.123.com:9001/vod/a.html 访问vmware中linux中的tomcat8081
通过访问 http://www.123.com:9002/edu/a.html 来将请求转发到两个不同的 tomcat8080 和 tomcat8081上
准备tomcat测试环境
在tomcat 8080 的/opt/tomcat8080/webapps/ 下新增edu/ 目录, 并在此目录中新增文件a.html, 文件内容如下:
<h4>tomcat 8080!</h4>
在tomcat 8081 的/opt/tomcat8081/webapps/ 下新增edu/ 目录, 并在此目录中新增文件a.html, 文件内容如下:
<h4>tomcat 8081!</h4>
配置nginx.conf
# config loadbalance nodes
upstream loadbalancer {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 9002;
server_name 192.128.101.127;
location ~ /edu/ {
proxy_pass http://loadbalancer;
}
}
启动tomcat和nginx
测试
http://www.123.com:9002/edu/a.html
轮询 为默认策略,请每个请求平均分配到后端的服务器,如果服务器down掉会自动剔除
权重策略,默认为1, 权重越高,被分配到的客户端数量越多,一般用于后端服务器性能不均匀的情况下。
# config loadbalance nodes
upstream loadbalancer {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=10;
}
8081的权重是8080的两倍,因此三次请求会有两次分配到8081,一次分配到8080。
将请求按照ip的hash结果分配,这样一个客户端可以固定访问一个后端服务器,这样可以解决session共享的问题。
sample:
当你服务端的一个特定url路径会被同一个用户连续访问时,如果负载均衡策略还是轮询的话,那该用户的多次访问会被打到各台服务器上,这显然并不高效(会建立多次http链接等问题)。甚至考虑一种极端情况,用户需要分片上传文件到服务器下,然后再由服务器将分片合并,这时如果用户的请求到达了不同的服务器,那么分片将存储于不同的服务器目录中,导致无法将分片合并。所以,此类场景可以考虑采用nginx提供的ip_hash策略。既能满足每个用户请求到同一台服务器,又能满足不同用户之间负载均衡。
# config loadbalance nodes
upstream loadbalancer {
ip_hash;
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=10;
server 127.0.0.1:8083 weight=10 down;
}
server {
listen 9002;
server_name 192.128.101.127;
location ~ /upload/ {
proxy_pass http://loadbalancer;
}
}
down关键字,表示下线的意思
将不同资源的请求按照url的hash结果分配,相关的资源只会被请求一次.
要用到urlhash,是要配合缓存命中来使用
sample
有一个服务器集群A,需要对外提供文件下载,由于文件上传量巨大,没法存储到服务器磁盘中,所以用到了第三方云存储来做文件存储。服务器集群A收到客户端请求之后,需要从云存储中下载文件然后返回,为了省去不必要的网络带宽和下载耗时,在服务器集群A上做了一层临时缓存(缓存一个月)。由于是服务器集群,所以同一个资源多次请求,可能会到达不同的服务器上,导致不必要的多次下载,缓存命中率不高,以及一些资源时间的浪费。在此类场景下,为了使得缓存命中率提高,很适合使用url_hash策略,同一个url(也就是同一个资源请求)会到达同一台机器,一旦缓存住了资源,再此收到请求,就可以从缓存中读取,既减少了带宽,也减少的下载时间。
upstream somestream {
hash $request_uri;
server 192.168.244.1:8080;
server 192.168.244.2:8080;
server 192.168.244.3:8080;
server 192.168.244.4:8080;
}
server {
listen 8081 default;
server_name test.csdn.net;
charset utf-8;
location /get {
proxy_pass http://somestream;
}
}
根据服务器的响应时间来分配,谁的响应时间短就优先分配
需要单独第三方fair模块
# config loadbalance nodes
upstream loadbalancer {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=10;
fair;
}
通过访问 http://www.123.com:9003/www/a.html 直接获取服务器www/a.html
通过访问 http://www.123.com:9003/images/ 直接获取服务器images/目录下的图片文件
cd /usr/local
mkdir nginx-test
cd nginx-test
mkdir www
mkdir images
cd www
vi a.html
存放如下内容
<h4>test static resource!!!</h4>
cd images
上传若干张图片到images
server {
listen 9003;
server_name 192.128.101.127;
location /www/ {
root /usr/local/nginx-test/;
expires 20s;
}
location /images/ {
root /usr/local/nginx-test/;
expires 20s;
autoindex on; #列出当前文件夹中的内容
}
}
http://www.123.com:9003/images/

http://www.123.com:9003/www/a.html

先通过主Nginx发送请求,如果主Nginx宕机,则通过备份Nginx发送请求。
keepalived 相当于路由,通过脚本来检测主节点是否可用,如果不可用,则自动切换到另外一台备用节点上。
虚拟IP ,通过虚拟IP来绑定到可用的Nginx节点, 先是绑定到master节点上,如果master节点宕机,会自动绑定到slave节点上

yum -y install keepalived
cd /etc/keepalived
echo > keepalived.conf
将如下内容粘贴到keepalived.conf
! Configuration File for keepalived
# 全局定义
global_defs {
notification_email {
acassen@firewall.loc
failover@firewall.loc
sysadmin@firewall.loc
}
notification_email_from Alexandre.Cassen@firewall.loc
smtp_server 192.168.200.1
smtp_connect_timeout 30
router_id 192.168.101.127 # 局域网keppalived主机标识,可以为主机名或IP
script_user root # 添加运行健康检查脚本的用户
enable_script_security # 添加运行健康检查脚本的组
}
# 检测脚本相关配置
vrrp_script chk_http_port {
script "/ect/keepalived/nginxchk/nginx_check.sh" # 检测脚本位置
interval 2 # 检测脚本执行的时间间隔
weight -20 #监测失败,则相应的 vrrp_instance 的优先级会减少20个点
}
# 虚拟IP相关配置
vrrp_instance VI_1 {
state MASTER # 备机上需要将 MASTER 改为 BACKUP
interface eth0 # 服务器使用的网卡名称,可以通过ifconfig查到
virtual_router_id 51 # 主机和备机的此配置项的值必须相同
priority 100 # 主机和备机的优先级,主机的值大于备机的值
advert_int 1 # 发送心跳时间间隔,默认为1秒
authentication { # 主备主机之间的认证表示信息
auth_type PASS #采用明文认证机制
auth_pass 1111 #编写明文密码
}
virtual_ipaddress {
192.168.101.150 # 虚拟IP地址,此参数备节点设置和主节点相同,此处可以绑定多个虚拟IP.
}
track_script {
chk_http_port #调用执行脚本
}
}
备机中配置keepalived.conf
! Configuration File for keepalived
# 全局定义
global_defs {
notification_email {
acassen@firewall.loc
failover@firewall.loc
sysadmin@firewall.loc
}
notification_email_from Alexandre.Cassen@firewall.loc
smtp_server SMTP.163.com
smtp_connect_timeout 30
router_id 192.168.101.128 # 局域网keppalived主机标识,可以为主机名或IP
script_user root # 添加运行健康检查脚本的用户
enable_script_security # 添加运行健康检查脚本的组
}
# 检测脚本相关配置
vrrp_script chk_http_port {
script "/etc/keepalived/nginx-chk/nginx_check.sh" # 检测脚本位置
interval 2 # 检测脚本执行的时间间隔
weight -20 # 监测失败,则相应的 vrrp_instance 的优先级会减少20个点
}
# 虚拟IP相关配置
vrrp_instance VI_1 {
state BACKUP # 备机上需要将 MASTER 改为 BACKUP
interface ens33 # 服务器使用的网卡名称,可以通过ifconfig查到
virtual_router_id 51 # 主机和备机的此配置项的值必须相同
priority 90 # 主机和备机的优先级,主机的值大于备机的值
advert_int 1 # 发送心跳时间间隔,默认为1秒
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.101.150 # 虚拟IP地址,此处可以绑定多个虚拟IP
}
track_script {
chk_http_port #调用执行脚本
}
}
启动主机和备机的keepalived
systemctl start keepalived.service
启动后,虚拟IP会先绑定到master节点上
ip a

C:\Windows\System32\drivers\etc\hosts
######## keepalived 虚拟IP ###########
192.168.101.150 vip
http://vip

关闭master节点
systemctl stop keepalived.service
cd /usr/local/nginx/sbin
./nginx -s stop
在备机slave01可以看到虚拟IP绑定到了slave01上
ip a

http://vip

一个master,可以有多个worker

客户端发送请求到master中,master收到请求任务后,worker通过争抢的机制从master处获取任务,然后worker负责具体的反向代理,负载均衡等具体的操作。

每个worker都是独立的一个进程,不需要单独加锁,从来避免了加锁带来的开销。
同时进程之间相互不影响,一个进程退出后不影响另外进程的正常工作。
一个master多个worker,可以方便线上做热部署
例如一个master对应四个worker,其中一个worker-1正在处理上一轮中争抢到的任务,此时nginx重新加载,则除过这个正在执行任务的worker外,其它三个worker都会加载最新的nginx配置内容并参与到新一轮的任务争抢中,worker-1在执行完手头的任务后也会自动加载最新的nginx配置,并参与到下一轮的任务争抢中去。
Nginx 同Redis类似都是采用io多路复用机制,每个worker都是一个进程,但每个进程里面只有一个主线程,通过异步非阻塞的方式处理请求,即使成千上万个请求也可以高效处理。每个worker可以将一个cpu的性能发挥到极致,所以worker数和cpu数保持一致最好,例如8核cpu设置8个worker,设置少了会浪费cpu资源,设置多了会造成cpu在上下文频繁切换造成的损耗。
问题1:客户端发送一个清求,占用几个worker连接数?
要么2个要么4个
如果客户端访问的是静态资源,占用两个worker连接数,如果客户端访问的是动态资源则会占用4个连接数。
问题2:nginx有一个master,有四个worker,每个worker支持的最大并发数为1024,那么支持的最大并发数为多少个 ?
如果客户端访问的是静态资源则会占用两个连接数,所以并发数为,1024x4/2
如果客户端访问的是动态资源则会占用4个连接数,所以并发数为, 1024x4/4
| 创建时间: | 2020/10/7 15:06 |
| 更新时间: | 2020/10/7 15:07 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/vitaminc4/article/details/76566720 |
ctrl + w 往回删除一个单词,光标放在最末尾
ctrl + u 删除光标以前的字符
ctrl + k 删除光标以后的字符
ctrl + a 移动光标至的字符头
ctrl + e 移动光标至的字符尾
ctrl + l 清屏
| 创建时间: | 2020/9/15 16:07 |
| 更新时间: | 2020/10/7 14:36 |
| 作者: | Chris |
| 来源: | https://blog.csdn.net/to_be_thebest/article/details/108082791 |
rpm --import /etc/pki/rpm-gpg/RPM*
yum makecache
| 创建时间: | 2020/10/7 14:13 |
| 更新时间: | 2020/10/7 14:15 |
| 作者: | Chris |
在整个hadoop的整个处理过程中都是利用ssh进行通讯的,就算是本机也知必须配置ssh免登录处理:
由于电脑上可能出现过ssh的相关配置,因此必须删除根目录下的".ssh"的文件夹
rm -rf ~/.ssh
在hadoopmaster 主机上生成ssh key
ssh-keygen -t rsa

把生成的共钥拷贝到授权的文件之中
root@hadoopmaster:~/.ssh# cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
测试免登录处理
root@hadoopmaster:~/.ssh# ssh root@hadoopmaster
使用exit退出运程登录
| 创建时间: | 2020/10/4 12:07 |
| 更新时间: | 2020/10/4 12:09 |
| 作者: | Chris |
| 来源: | http://chao58.top/blog/read/92 |
| 创建时间: | 2020/9/15 14:57 |
| 更新时间: | 2020/9/25 20:58 |
| 作者: | Chris |
RabbitMQ是Erlang语言编写的,实现了高级消息队列协议(AMQP)的开源的消息中间件
- 同步变异步
- 解耦,高内聚低耦合,对新增开放,对修改关闭
- 流量削峰,一般用在秒杀服务



Message 包含
用来接收消息并把消息路由给队列
三种常用的交换器
- direct 发布与订阅,完全匹配,默认
- fanout 广播
- topic 主题,规则匹配
RabbitMQ决定消息发送到哪个queue的规则队列通过路由键绑定到交换器
当消息发送到RabbitMQ时,RabbitMQ会将消息中和绑定中使用的路由键进行匹配
如果匹配则发送到该队列,如果不匹配则将消息发进黑洞,黑洞是指这个消息没有一个消费者
基于路由键将消息队列和交换器之间关联起来的路由规则,可以将交换器理解为一个由绑定构成的路由表。
队列可以存储很多的消息,基本上它是一个没有限制的缓冲区,前提是你的机器有足够的存储空间
多个生产者可以将信息发给同一个队列,多个消费者也可以从同一队列读取数据。
消息的生产者与消费者同RabbitMQ建立的TCP连接
表示消息队列服务器实体
下载: wget http://www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
安装: rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
检验erl
[root@iZ25sabz8p5Z sbin]# erl
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]
退出用 `halt().`
下载: wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
安装: rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
下载: wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
安装: rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
Error: Cannot retrieve metalink for repository: epel. Please verify its path and try again
处理很简单,修改文件“/etc/yum.repos.d/epel.repo”, 将baseurl的注释取消, mirrorlist注释掉。即可。
安装问题参考:
https://blog.csdn.net/yunfeng482/article/details/72853983
https://www.cnblogs.com/mcgrady/p/7614417.html
开启rabbitmq
sudo service rabbitmq-server start
Error epmd error for host xxx: address (cannot connect to host/port)
vi /etc/rabbitmq/rabbitmq-env.conf文件,添加NODENAME=rabbit@localhost
开启管理页面插件
sudo rabbitmq-plugins enable rabbitmq_management
添加管理员账号
sudo rabbitmqctl add_user chris 123456
分配用户标签
sudo rabbitmqctl set_user_tags chris administrator
创建和赋角色完成后查看并确认
sudo rabbitmqctl list_users
设置用户权限
sudo rabbitmqctl set_permissions -p "/" chris ".*" ".*" ".*"
web页面访问rabbitmq
http://master:15672/
package com.springboot.chris.demo;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SenderConfig {
@Bean
public Queue queue() {
return new Queue("hello-chris-queue");
}
}
package com.springboot.chris.demo;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class Sender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send() {
String msg = "hello" + new Date();
rabbitTemplate.convertAndSend("hello-chris-queue", msg);
}
}
package com.springboot.chris.demo;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Receiver {
@RabbitListener(queues = "hello-chris-queue")
public void process(String msg) {
System.out.println("reciver:" + msg);
}
}
application.properties
spring.application.name=springboot-amqp
server.port=8080
spring.rabbitmq.host=hadoop
spring.rabbitmq.port=5672
spring.rabbitmq.username=chris
spring.rabbitmq.password=123456
| 创建时间: | 2020/9/22 9:52 |
| 更新时间: | 2020/9/22 9:53 |
| 作者: | Chris |
https://www.inneed.club/resources/index
| 创建时间: | 2020/9/11 10:58 |
| 更新时间: | 2020/9/14 12:57 |
| 作者: | Chris |
| 来源: | about:blank |
| 创建时间: | 2020/9/10 10:30 |
| 更新时间: | 2020/9/10 10:30 |
| 作者: | Chris |
https://blog.csdn.net/kingtok/article/details/104963122
脑图笔记地址: https://www.processon.com/view/link/5e68624ce4b00b99c1debca5#map
源码:https://github.com/TNoOne/cloud2020
笔记:https://blog.csdn.net/qq_41211642/article/details/104772140
QPS
https://www.cnblogs.com/longxiaojiangi/p/9259745.html
| 创建时间: | 2020/9/2 15:46 |
| 更新时间: | 2020/9/6 17:29 |
| 作者: | Chris |
当初始化时会创建 一个名为SpringSecurityFilterChain的servlet过滤器,类型为FilterChainProxy.
SecurityContextPersistenceFilter 整个拦截过程的入口和出口
UsernamePasswordAuthenticationFilter 帐号密码的认证由此拦截器拦截处理
FilterSecurityInterceptor 用于保护web资源,使用 AccessDecisionManager 对当前用户进入授权访问
AuthenticationManager 校验用户身份
AccessDecisionManager 校验用户权
用户认证成功后,为了避免用户每次操作都进行认证,可以将用户的身份信息放在会话中进行管理,SpringSecurity提供了会话管理,将认证成功的身份信息放在SecurityContextHolder上下文,SecurityContext上下文是和当前线程绑定的。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
authentication.getPrincipal()
ifRequired:默认情况下SpringSecurity会为每一个登录成功的用户新建一个Session。
always:如果没有就新建一个。
never: SpringSecurity 对登录成功的用户不创建Session,但如果应用程序里面某个地方新建了Session,那么SpringSecurity会用它。
stateless: SpringSecurity 对登录成功的用户不创建Session, 你的应用程序里面也不会允许新建Session,并且它不使用cookie,所以每个请求都需要重新验证身份,这种无状态架构适用于RestAPI及无状态认证机制。
SpringSecurity 通过http.authorizeRequests()对web请求进行授权保护
SpringSecurity通过投票的机制来验证授权
AffirmativeBased()
ConsensusBased()
UnanimousBased()
授权方式包括Web授权和方法授权
Web授权:通过URL拦截进行授权, FilterSecurityInterceptor
方法授权:通过方法拦截进行授权, MethodSecurityInterceptor
SpringSecurity为了防止CSRF的发生,限制了除get以外的大多数方法。
| 创建时间: | 2020/9/3 22:08 |
| 更新时间: | 2020/9/3 22:11 |
| 作者: | Chris |

why
配置统一存放,管理
为什么要用
配置文件vs配置中心
便捷性:配置文件不适合集群,分布式场景
安全性:统一存储,通过跳板机
实时性:配置改完不需要重启服务,可以自动更新
how 配置中心如何使用
配置中心本质就是一个系统
功能:
对于配置信息的CRUD,并提供接口给应用程序访问
实现原理:本质存储配置信息
信息可以存储的地方:mysql,redis,mq,nacos
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务

配置中心服务端:
本质和数据库一样用来存储数据
使用树形结点
配置中心客户端:
从ZK读取配置信息加载到Spring容器中

zkui

@Autowired
Environment environment
| 创建时间: | 2020/9/2 16:22 |
| 更新时间: | 2020/9/2 16:22 |
| 作者: | Chris |
海史密斯将适应型领导力定义为两个维度:保持敏捷和行事敏捷。
保持敏捷包括:注重敏捷项目管理的基石,比如增量交付,持续整合和适应变动的需求。
敏捷领导者必须进行的“行事敏捷”活动包括:少做;价值的速度,质量,吸引和激励。
| 创建时间: | 2020/9/2 16:19 |
| 更新时间: | 2020/9/2 16:21 |
| 作者: | Chris |
- Individuals and interactions over processes and tools
- Working software over comprehensive documentation
- Customer collaboration over contract negotiation
- Responding to change over following a plan
Principles behind the Agile Manifesto
We follow these principles:
- Our highest priority is to satisfy the customer through early and continuous delivery of valuable software.
- Welcome changing requirements, even late in development. Agile processes harness change for the customer's competitive advantage.
- Deliver working software frequently, from a couple of weeks to a couple of months, with a preference to the shorter timescale.
- Business people and developers must work together daily throughout the project.
- Build projects around motivated individuals. Give them the environment and support they need, and trust them to get the job done.
- The most efficient and effective method of conveying information to and within a development team is face-to-face conversation.
- Working software is the primary measure of progress.
- Agile processes promote sustainable development. The sponsors, developers, and users should be able to maintain a constant pace indefinitely.
- Continuous attention to technical excellence and good design enhances agility.
- Simplicity, the art of maximizing the amount of work not done, is essential.
- The best architectures, requirements, and designs emerge from self-organizing teams.
- At regular intervals, the team reflects on how to become more effective, then tunes and adjusts its behavior accordingly.

Scrum 偏重于过程,XP 则偏重于实践,但是实际中,两者是结合一起应用的。

in each sprint we deliver the [Minimum Viable Product,MVP] to customer 对用户有价值的最小可用产品
Sprint Planning Meeting ( Sprint 计划会议)来从中挑选出一个 Story 作为本次迭代完成的目标
Daily meeting
Weekly meeting
Sprint Retrospective Meeting(回顾会议)
TDD是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。
TDD 有三层含义:
- Task-Driven Development,任务驱动开发,要对问题进行分析并进行任务分解。
- Test-Driven Development,测试驱动开发。
- Test-Driven Design,测试保护下的设计改善。TDD 并不能直接提高设计能力,它只是给你更多机会和保障去改善设计。
| 创建时间: | 2020/9/2 16:18 |
| 更新时间: | 2020/9/2 16:18 |
| 作者: | Chris |
使用 = 和 - 标记一级和二级标题
*斜体文本*
_斜体文本_
**粗体文本**
__粗体文本__
***粗斜体文本***
___粗斜体文本___
你可以在一行中用三个以上的 星号、减号、底线
来建立一个分隔线,行内不能有其他东西
你也可以在星号或是减号中间插入空格。
下面每种写法都可以建立分隔线:
***
* * *
*****
- - -
----------
如果段落上的文字要添加删除线,只需要在文字的两端加上两个波浪线 ~~ 即可,实例如下
RUNOOB.COM
GOOGLE.COM
BAIDU.COM
<u>带下划线文本</u>
创建脚注格式类似这样 [^RUNOOB]。
创建脚注格式类似这样 [^Chris]。
创建脚注格式类似这样 [^RUNOOB]
创建脚注格式类似这样 [^Chris]
[^RUNOOB]: 菜鸟教程 -- 学的不仅是技术,更是梦想!!!
[^Chris]: A smart and intellient man
Markdown 支持有序列表和无序列表。
无序列表使用星号(*)、加号(+)或是减号(-)作为列表标记:
有序列表使用数字并加上 . 号来表示,如:
列表嵌套只需在子列表中的选项添加四个空格即可:
区块引用
菜鸟教程
学的不仅是技术更是梦想
最外层
第一层嵌套
第二层嵌套
区块中使用列表
- 第一项
- 第一项
- 第二项
- 第三项
- 第二项
- 第一项
- 第二项
- 第三项
菜鸟教程
学的不仅是技术更是梦想
如果是段落上的一个函数或片段的代码可以用反引号把它包起,例如:
printf() 函数
代码区块
代码区块使用 4 个空格或者一个制表符(Tab 键)。实例如下:
$(document).ready(function(){
alert('RUNOOB');}
);
$(document).ready(function () {
alert('RUNOOB');
});
from math import hypot
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)
def __abs__(self):
return hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)


Markdown 制作表格使用 | 来分隔不同的单元格,使用 - 来分隔表头和其他行。
| 表头 | 表头 |
|---|---|
| 单元格 | 单元格 |
| 单元格 | 单元格 |
我们可以设置表格的对齐方式:
-: 设置内容和标题栏居右对齐。
:- 设置内容和标题栏居左对齐。
:-: 设置内容和标题栏居中对齐。
| 左对齐 | 右对齐 | 居中对齐 |
|---|---|---|
| 单元格 | 单元格 | 单元格 |
| 单元格 | 单元格 | 单元格 |
支持的 HTML 元素
不在 Markdown 涵盖范围之内的标签,都可以直接在文档里面用 HTML 撰写。
目前支持的 HTML 元素有:<kbd> <b> <i> <em> <sup> <sub> <br>等 ,如:
使用 <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Del</kbd> 重启电脑
转义
Markdown 使用了很多特殊符号来表示特定的意义,如果需要显示特定的符号则需要使用转义字符,Markdown 使用反斜杠转义特殊字符:
**文本加粗**
\*\* 正常显示星号 \*\*
Markdown 支持以下这些符号前面加上反斜杠来帮助插入普通的符号:
\ 反斜线
` 反引号
* 星号
_ 下划线
{} 花括号
[] 方括号
() 小括号
# 井字号
+ 加号
- 减号
. 英文句点
! 感叹号
| 创建时间: | 2020/9/2 16:17 |
| 更新时间: | 2020/9/2 16:18 |
| 作者: | Chris |
Joplin是一个免费的开源笔记记录和待办事项应用程序,可以处理笔记本中组织的大量笔记。Evernote导出的笔记可以直接导入到Joplin中,同时也可以导入普通Markdown文件。通过WebDAV您可以直接将Joplin中的笔记同步到坚果云。
以移动端为例:
1.由于Joplin的同步需要指定文件夹,因此在连接webdav前,请先在坚果云上创建一个英文名称的根目录文件夹,如“Joplin”
2.打开Joplin的配置界面,在同步中选择WebDAV
WebDAV URL : https://dav.jianguoyun.com/dav/Joplin
(注:需要在服务器地址后面添加刚才创建的文件夹的名称)
WebDAV 用户名:您的坚果云账号邮箱
WebDAV 密码:在坚果云中生成的第三方应用密码(如何生成应用授权密码)
3.点击“检查同步配置”,提示连接成功代表您已成功将Joplin与坚果云进行关联。

| 创建时间: | 2020/9/2 15:44 |
| 更新时间: | 2020/9/2 15:44 |
| 作者: | Chris |
https://www.cnblogs.com/lifengdi/archive/2019/09/20/11554923.html
| 创建时间: | 2020/9/2 15:42 |
| 更新时间: | 2020/9/2 15:43 |
| 作者: | Chris |
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols if ord(symbol) > 127]
print(codes)
codes = list(filter(lambda c: c > 127, map(ord, symbols)))
print(codes)
不管是哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。
列表推导<kbd>(list comprehension)</kbd> 简称为 listcomps,<kbd>生成式表达器</kbd> (generator expression)则称为 genexps
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print(codes)
36, 162, 163, 165, 8364, 164]
列表推导可以生成两个或以上的可迭代类型的笛卡儿积。 笛卡儿积是一个列表,列表里的元素是由输入的可迭> 代类型的元素对构成的元组,因此笛卡儿积列表的长度等于输入变量的长度的乘积
self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已
如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
<kbd>生成器表达式逐个产出元素,从来不会一次性产出一个含有 6 个 T 恤样式的列表</kbd>
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in ((c, s) for c in colors for s in sizes):
print(tshirt)
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。 正是这个位置信息给数据赋予了意义。
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素.
for country, _ in traveler_ids: print(country)
最好辨认的元组拆包形式就是平行赋值,也就是说把一个可迭代对象里的元素,一并赋值
到由对应的变量组成的元组中
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates # 元组拆包
函数用 *args 来获取不确定数量的参数算是一种经典写法了
a, b, *rest = range(5)
(0, 1, [2, 3, 4])
在平行赋值中, * 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
a, *body, c, d = range(5)
a, body, c, d
(0, [1, 2], 3, 4)
接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d)) 。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
Card = collections.namedtuple('Card', ['rank', 'suit'])
创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print(tokyo.country)
print(tokyo.population)
print(tokyo[0])
_fields 属性是一个包含这个类所有字段名称的元组。
_make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟City(*delhi_data) 是一样的。
_asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来
print(City._fields)
LatLong = namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data)
print(delhi._asdict())
for key, value in delhi._asdict().items():
print(key + ':', value)

当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素range(3)和 my_list[:3] 都返回 3 个元素
mylist = [1, 2, 3, 4, 5, 'a', 'b', 'c']
print(mylist[:2])
print(mylist[2:])
[1, 2]
[3, 4, 5, 'a', 'b', 'c']
可以用 s[a: b : c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。
c 的值还可以为负,负值意味着反向取值。
s = 'bicycle'
print(s[::3])
print(s[::-1])
print(s[::-2])
bye
elcycib
eccb
deck[12::13] 的形式在未洗过的牌里把每种花色的 A 拿出来
如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,
也要把它转换成可迭代的序列
l = list(range(10))
l[2:5] = [20, 30]
print(l)
del l[5:7]
print(l)
l[3::2] = [11, 22]
print(l)
# l[2:5] = 100
l[2:5] = [100]
print(l)
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
[0, 1, 20, 11, 5, 22, 9]
[0, 1, 100, 22, 9]
+ 和 * 都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列
l = [1, 2, 3]
l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
5 * 'abcd'
'abcdabcdabcdabcdabcd'
建立由列表组成的列表
list_temp = [['_'] * 3 for i in range(3)]
print(list_temp)
list_temp[1][2] = "X"
print(list_temp)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
+= 和 *= 的表现取决于它们的第一个操作对象, += 背后的特殊方法是 __iadd__ (用于“就地加法”) *= 背后的特殊方法是 __imul__ (用于“就地加法”)
- 如果a 实 现 了 __iadd__ 方 法, 就 会 调 用 这 个 方 法。 同 时 对 可 变 序 列( 例 如 list 、bytearray 和 array.array )来说,a 会就地改动
- 如果 a 没有实现 __iadd__ 的话, a += b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a + b ,得到一个新的对象,然后赋值给 a
lst = list(range(3))
id_list = id(lst)
print("id_list", id_list)
lst += ["A", "B"]
print(lst)
id_list = id(lst)
print("id_list:", id_list)
tpl = tuple(range(3))
id_tuple = id(tpl)
print("id_tuple:", id_tuple)
tpl += ("A", "B")
print(tpl)
id_tuple = id(tpl)
print("id_tuple:", id_tuple)
id_list 1798125102280
[0, 1, 2, 'A', 'B']
id_list: 1798125102280
id_tuple: 1798125055864
(0, 1, 2, 'A', 'B')
id_tuple: 1798121809352
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素
是一个对 Python 运行原理进行可视化分析的工具
list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表
如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None ,好让调用者知道传入的参数发生了变动,而且并未产生新的对象
sorted 它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管sorted 接受的是怎样的参数,它最后都会返回一个列表。
不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。
reverse 默认为False 升序排列
fruits = ['grape', 'raspberry', 'apple', 'banana']
print(sorted(fruits))
print(sorted(fruits, reverse=True))
print(sorted(fruits, key=len, reverse=True))
['apple', 'banana', 'grape', 'raspberry']
['raspberry', 'grape', 'banana', 'apple']
['raspberry', 'banana', 'grape', 'apple']
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。
数组支持所有跟可变序列有关的操作,包括 .pop 、 .insert 和 .extend 。
数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile 。
from array import array
from random import random
float_array_1 = array('d', (random() for x in (range(10 ** 7))))
first = float_array_1[0]
last = float_array_1[-1]
print(first, last)
fb = open("./float.bin", "wb")
float_array_1.tofile(fb)
fb.close()
float_array_2 = array('d')
fb = open("./float.bin", "rb")
# 把 1000 万个浮点数从二进制文件里读取出来
float_array_2.fromfile(fb, 10 ** 7)
fb.close()
first = float_array_2[0]
last = float_array_2[-1]
print(first, last)
# 检查两个数组的内容是不是完全一样
if float_array_1 == float_array_2:
print(True)
else:
print(False)
| 创建时间: | 2020/9/2 15:42 |
| 更新时间: | 2020/9/2 15:42 |
| 作者: | Chris |
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple faker
简体中文:
繁体中文:zh_TW
美国英文:en_US
英国英文:en_GB
德文:de_DE
日文:ja_JP
韩文:ko_KR
法文:fr_FR
Faker 库在设计上,为了解耦,将 Provider 对象做成了 Faker 对象的”插件“。Faker 可以添加一个个 Provider 对象,Provider 对象为 Faker 对象提供了生成某项数据的核心实现。
Address,用于生成一些和地址相关的数据,如地址、城市、邮政编码、街道等内容, 用法如下:
from faker import Faker
faker = Faker("zh_CN")
address = faker.address()
building_number = faker.building_number()
city = faker.city()
cityname = faker.city_name()
city_suf = faker.city_suffix()
country = faker.country()
country_2 = faker.country_code(representation="alpha-2")
district = faker.district()
postcode = faker.postcode()
province = faker.province()
street_address = faker.street_address()
street_name = faker.street_name()
street_suffix = faker.street_suffix()
print("address:", address)
print("building_number:", building_number)
print("city:", city)
print("cityname:", cityname)
print("city_suf:", city_suf)
print("country:", country)
print("country_2:", country_2)
print("district:", district)
print("postcode:", postcode)
print("province:", province)
print("street_address:", street_address)
print("street_name:", street_name)
print("street_suffix:", street_suffix)
用于生成和颜色相关的数据,如 HEX、RGB、RGBA 等格式的颜色,用法如下:
from faker import Faker
faker = Faker()
color_name = faker.color_name()
hex_color = faker.hex_color()
rgb_color = faker.rgb_color()
rgb_css_color = faker.rgb_css_color()
safe_color_name = faker.safe_color_name()
safe_hex_color = faker.safe_hex_color()
print("color_name :", color_name)
print("hex_color :", hex_color)
print("rgb_color :", rgb_color)
print("rgb_css_color :", rgb_css_color)
print("safe_color_name :", safe_color_name)
print("safe_hex_color :", safe_hex_color)
| 创建时间: | 2020/9/2 15:41 |
| 更新时间: | 2020/9/2 15:41 |
| 作者: | Chris |
pip --version
pip install camelcase
在 https://pypi.org/,您可以找到更多的包
pip list
python -m pip install --upgrade pi
dir(__builtins__)
help(int)
pip uninstall pip install camelcase
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mysql-connector
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openpyxl
pip install openpyxl --ignore-installed openpyxl
| 创建时间: | 2020/9/2 15:41 |
| 更新时间: | 2020/9/2 15:41 |
| 作者: | Chris |
| 创建时间: | 2020/9/2 15:40 |
| 更新时间: | 2020/9/2 15:41 |
| 作者: | Chris |
APScheduler
https://www.cnblogs.com/zhaoyingjie/p/9664081.html
https://www.cnblogs.com/Neeo/p/10435059.html
https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html
skipped: maximum number of running instances reached
https://www.cnblogs.com/yongqing/p/9595987.htm
repr()函数将对象转化为供解释器读取的形式
https://www.runoob.com/python/python-func-repr.html
Difference between str and repr in Python
http://stackoverflow.com/questions/1436703/difference-between-str-and-repr-in-python
Python获取指定文件夹下的文件
https://www.cnblogs.com/yuanyongqiang/p/11714281.html
Deep Thoughts by Raymond Hettinger
https://rhettinger.wordpress.com/2011/05/26/super-considered-super/
https://rhettinger.wordpress.com/
https://blog.lilydjwg.me/2014/3/13/python-3-s-super-and-__class__.43499.html
python命名规范
https://www.jianshu.com/p/4bd28460d912
Python程序员常犯的10个错误
http://bookshadow.com/weblog/2014/05/14/top-10-mistakes-that-python-programmers-make/
PyCharm中Python代码提示:Shadows name from outer scope
https://www.crifan.com/pycharm_python_code_notice_shadows_name_from_outer_scope/
python实现十大经典算法
https://www.cnblogs.com/123blog/p/10405443.html
https://blog.csdn.net/coxhuang/article/details/90381254
DeepClone
https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-deep.html
python 字符串前面加u,r,b的含义
https://www.cnblogs.com/liangmingshen/p/9274021.html
r"\n\n\n\n” #表示一个普通生字符串 \n\n\n\n,而不表示换行了。
https://blog.csdn.net/Kwoky/article/details/84754901
格式化字符串常量(formatted string literals),是Python3.6新引入的一种字符串格式化方法,
该方法源于PEP 498 – Literal StringI nterpolation,主要目的是使格式化字符串的操作更加简便。
f-string在形式上是以 <kbd>f</kbd> 或 <kbd>F</kbd> 修饰符引领的字符串(f'xxx' 或 F'xxx'),
以大括号 <kbd>{}</kbd> 标明被替换的字段;
f-string在本质上并不是字符串常量,而是一个在运行时运算求值的表达式
comedian = {'name': 'Eric Idle', 'age': 74}
f"The comedian is {comedian['name']}, aged {comedian['age']}."
https://www.cnblogs.com/Agoni-7/p/11129296.html
https://stackoverflow.com/questions/13694143/parsing-cdata-in-xml-with-python?r=SearchResults
https://www.cnblogs.com/zhangxinqi/p/9210211.html
https://blog.csdn.net/AlanGuoo/article/details/95239795
https://www.cnblogs.com/hanmk/p/9843136.html
https://www.lizenghai.com/archives/1911.html
https://docs.python.org/3/reference/datamodel.html
https://www.cnblogs.com/programmer-tlh/p/10461353.html
| 创建时间: | 2020/9/2 15:38 |
| 更新时间: | 2020/9/2 15:40 |
| 作者: | Chris |
Insert into select 请慎用,同事因为使用了 Insert into select 语句引发了重大生产事故,最后被开除。
某天 xxx 接到一个需求,需要将表 A 的数据迁移到表 B 中去做一个备份。他本想通过程序先查询查出来然后批量插入,但 xxx 觉得这样有点慢,需要耗费大量的网络 I/O,决定采取别的方法进行实现。
通过在某度的海洋里遨游,他发现了可以使用 insert into select 实现,这样就可以避免使用网络 I/O,直接使用 SQL 依靠数据库 I/O 完成,这样简直不要太棒,然后他就被开除了。
事故发生的经过
由于数据数据库中 order_today 数据量过大,当时好像有 700W 了,并且每天在以 30W 的速度增加。
所以上司命令 xxx 将 order_today 内的部分数据迁移到 order_record 中,并将 order_today 中的数据删除,这样来降低 order_today 表中的数据量。
由于考虑到会占用数据库 I/O,为了不影响业务,计划是 9:00 以后开始迁移,但是 xxx 在 8:00 的时候,尝试迁移了少部分数据(1000 条),觉得没啥问题,就开始考虑大批量迁移。
在迁移的过程中,应急群是先反应有小部分用户出现支付失败,随后反应大批用户出现支付失败的情况,以及初始化订单失败的情况,同时腾讯也开始报警。
然后 xxx 就慌了,立即停止了迁移。本以为停止迁移就就可以恢复了,但是并没有。
后面发生的你们可以脑补一下,当时整个支付系统瘫痪了快一个小时,客服电话都被打爆。
事故还原
在本地建立一个精简版的数据库,并生成了 100w 的数据。模拟线上发生的情况。
建立表结构
订单表如下:
CREATE TABLE `order_today` (
`id` VARCHAR ( 32 ) NOT NULL COMMENT '主键',
`merchant_id` VARCHAR ( 32 ) CHARACTER SET utf8 COLLATE utf8 _general_ci NOT NULL COMMENT '商户编号',
`amount` DECIMAL ( 15, 2 ) NOT NULL COMMENT '订单金额',
`pay_success_time` datetime NOT NULL COMMENT '支付成功时间',
`order_status` VARCHAR ( 10 ) CHARACTER SET utf8 COLLATE utf8 _general_ci NOT NULL COMMENT '支付状态 S:支付成功、F:订单支付失败',
`remark` VARCHAR ( 100 ) CHARACTER SET utf8 COLLATE utf8 _general _ci DEFAULT NULL COMMENT '备注',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT _TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT _TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间 -- 修改时自动更新',
PRIMARY KEY ( `id` ) USING BTREE,
KEY `idx_merchant_id` ( `merchant_id` ) USING BTREE COMMENT '商户编号'
) ENGINE = INNODB DEFAULT CHARSET = utf8;
订单记录表如下:
CREATETABLE order_record like order_today;
今日订单表数据如下:

模拟迁移
把 8 号之前的数据都迁移到 order_record 表中去:
INSERT INTO order_record SELECT * FROM order_today WHERE pay_success_time < '2020-03-08 00:00:00';
在 Navicat 中运行迁移的 SQL,同时开另个一个窗口插入数据,模拟下单:

从上面可以发现一开始能正常插入,但是后面突然就卡住了,并且耗费了 23s 才成功,然后才能继续插入。这个时候已经迁移成功了,所以能正常插入了。
出现的原因
在默认的事务隔离级别下:insert into order_record select * from order_today 加锁规则是:order_record 表锁,order_today 逐步锁(扫描一个锁一个)。
分析执行过程:

通过观察迁移 SQL 的执行情况你会发现 order_today 是全表扫描,也就意味着在执行 insert into select from 语句时,MySQL 会从上到下扫描 order_today 内的记录并且加锁,这样一来不就和直接锁表是一样了。
这也就可以解释,为什么一开始只有少量用户出现支付失败,后续大量用户出现支付失败,初始化订单失败等情况,因为一开始只锁定了少部分数据,没有被锁定的数据还是可以正常被修改为正常状态。
由于锁定的数据越来越多,就导致出现了大量支付失败。最后全部锁住,导致无法插入订单,而出现初始化订单失败。
解决方案
由于查询条件会导致 order_today 全表扫描,什么能避免全表扫描呢,很简单嘛,给 pay_success_time 字段添加一个 idx_pay_suc_time 索引就可以了。
由于走索引查询,就不会出现扫描全表的情况而锁表了,只会锁定符合条件的记录。
最终的 SQL:
INSERTINTO order_record SELECT * FROM order_today FORCE INDEX (idx_pay_suc_time) WHERE pay_success_time <= '2020-03-08 00:00:00';
执行过程如下:

总结
使用 insert into table A select * from table B 语句时,一定要确保 table B 后面的 where,order 或者其他条件,都需要有对应的索引,来避免出现 table B 全部记录被锁定的情况。
| 创建时间: | 2020/9/2 15:32 |
| 更新时间: | 2020/9/2 15:33 |
| 作者: | Chris |
mapper.xml文件中resultMap的type或者parameterType会使用自定义的pojo,
此时可以用完全限定名来指定这些POJO的引用,例如
<select resultType="com.e3mall.cmsao.mapper.User">
或者你可以通过在application.properties中指定POJO扫描包来让mybatis自动扫描到自定义POJO
application.properties
mybatis.type-aliases-package=com.e3mall.cms.dao.mapper
application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
#所有Entity别名类所在包
type-aliases-package: com.chris.springcloud.payment.main.entities
| 创建时间: | 2020/9/2 15:30 |
| 更新时间: | 2020/9/2 15:30 |
| 作者: | Chris |
| Protocol | Default port number |
|---|---|
| https | 443 |
| http | 80 |
| ftp | 21 |
| sftp/SSH | 22 |
sftp -oPort=22 sftpHost
sftp -oPort=22 userName@sftpHost
sftp -oPort=22 82.196.50.13
sftp -oPort=22 ilayer_uat@82.196.50.13
nc 82.196.50.13 22
ftp 10.205.5.75 21
telnet 219.139.242.199 22
An useful option to check connectivity for FTP/SFTP is <kbd>nc</kbd>. In this case we provide the proxy setup. For example:
/usr/bin/nc -v omcs-proxy.oracleoutsourcing.com 80 mft-ftp.cegedim-srh.net 22
ping vmohssgds061.oracleoutsourcing.com -l 950 -n 100
tracert -d vmohssgds061.oracleoutsourcing.com
you can include the details of the proxy by using option –x
curl -x omcs-proxy.oracleoutsourcing.com:80 -k -v https://61.183.154.3:443/jdcj?wsdl
curl -x omcs-proxy.oracleoutsourcing.com:80 -k -v http://61.183.154.3:8182/jdcj?wsdl
if you want to download a file, you can use curl with the -O or -o options. The former will save the file in the current working directory with the same name as in the remote location, whereas the latter allows you to specify a different filename and/or location.
$ curl -OL http://yourdomain.com/yourfile.tar.gz
$ curl -oL newfile.tar.gz http://yourdomain.com/yourfile.tar.gz
$ curl -u username:password -O ftp://yourftpserver/yourfile.tar.gz
$ curl -u username:password -T mylocalfile.tar.gz ftp://yourftpserver
| 创建时间: | 2020/9/2 15:27 |
| 更新时间: | 2020/9/2 15:29 |
| 作者: | Chris |
bash file returns unexpected token `$'do\r''
apt-get install dos2unix
dos2unix my_script.sh
#!/bin/bash --用哪个命令编译器来解释,放在第一行
执行shell脚本的方法有两种
1. bash script.sh --不需要对脚本写解析器,
2. ./script.sh
每两分钟执行一次
crontab -e
*/2 * * * * /home/chris/shells/clearftpdir.sh
| crontab -l --查看系统中现有的定时任务 |
|---|
| crontab -u root -l --查看root用户下的定时任务 |
| 用户自定义变量:由用户自已定义修改和使用 |
|---|
| shell预定义变量: |
| 位置变量:通过命令行给程序传递执行参数,总共可以接收九个位置变量$1~$9 |
v_name=value
$v_name or ${v_name}
read -p "msg" v_name
unset v_name
位置变量:从$1 到$9
数学运算:
total=expre $1 + $2
total=$(($1+$2))
+
-
* total=$(($1*$2))
/
%
| $# 命令行中位置参数的个数 |
|---|
| $* 所有位置参数的内容 |
| $? 上一条命令执行后返回的状态,0表示执行成功,非0表示执行失败 |
| $0 当前执行的进程/程度名 |
对引号"":可以解释$引用的变量,不能解释转义字符
单引号'': 不能解释变量,$被视为普通字符,不能解释转义字符
反引号``:用来解释命令并把命令的返回值输出给变量。
--解释输出中的转义字符
echo -e "my name is chris*\n*my name is user1"
--输出不换行等待键盘输入
echo -n "pls input your name"
read name
read -p "pls input your name:" name
echo "my name is $name"
echo -e "httpd process \033[32;40m[ok]\033[0m"
\033[前景色;背景色m
\033[0m --恢复到系统默认的颜色, 不然往后的颜色都会被改变
\033[30m -- \033[37m 设置前景色(字体颜色)
\033[30m 将字符的显示颜色改为黑色
\033[31m 将字符的显示颜色改为红色
\033[32m 将字符的显示颜色改为绿色
\033[33m 将字符的显示颜色改为淡红色
\033[34m 将字符的显示颜色改为蓝色
\033[35m 将字符的显示颜色改为紫色
\033[36m 将字符的显示颜色改为淡蓝色
\033[37m 将字符的显示颜色改为灰色
\033[40m -- \033[47m 设置背景色
\033[40m 将背景色设置为黑色
\033[41m 将背景色设置为红色
\033[42m 将背景色设置为绿色
\033[43m 将背景色设置为淡红色
\033[44m 将背景色设置为蓝色
\033[45m 将背景色设置为紫色
\033[46m 将背景色设置为淡蓝色
\033[47m 将背景色设置为灰色
--输出前n行
cat /etc/passwd | head -n
--输出后n行
cat /etc/passwd | tail -n
--输出的同时并将输出的内容保存到另一个文件中
cat /etc/passwd | tee copy.1
cat<<x
pls choose your name:
1>chris
2>john
3>jeffery
4>frederic
x
nl --number lines of files
cat /etc/passwd | head -n | nl
nl /etc/passwd
nl /etc/passwd >> mypass.txt
条件测试
test 测试表达式是否成立,成立返回0否则返回其它值
测试文件状态
格式:[ 操作符 文件或目录 ]
操作符
-d 测试是否为目录
-e 测试目录或文件是否存在
-f 测试是否为文件
-r 测试当前用户是否有read权限
-w 测试当前用户是否有write权限
-x 测试当前用户是否有excute权限
-L 测试是否为link文件

字符串比较
格式:
[str1 = str2]
[str1 != str2]
[-z str] --判断字符串是否为空
整数值比较
[ 整数1 操作符 整数2]
-eq
-ne
-gt
-lt
-le
-ge

逻辑测试
-a &&
-o ||
!
if 语句
if [ $score -lt 60 ]; then
echo '60以下'
elif [ $score -ge 60 ] && [ $score -lt 70 ]; then
echo '60-70之间'
elif [ $score -ge 70 ] && [ $score -lt 80 ]; then
echo '70-80之间'
elif [ $score -gt 80 ] && [ $score -lt 90 ]; then
echo '80-90之间'
else
echo '90以上!'
fi
case 语句
weekday=date +%w
case $weekday in
1 )
echo 'Monday'
;;
2 )
echo 'Tuesday'
;;
esac
while 语句
num=3
tot=0
while [ $num -gt 0 ]; do
echo $num
tot=((tot+$num))
num=((num-1))
done
for 语句
for i in user0 user1 user2 user3 user4 user5
do
echo $i
userdel -r $i
done
for (( i = 1; i <= 10; i++ )); do
echo $i
user="user$i"
userdel -r $user
done
--skip the user5
for (( i = 1; i <= 10; i++ )); do
echo $i
user="user$i"
if [ $i -eq 5 ]; then
continue
fi
userdel -r $user
done
--input 1 to 5
for (( i = 1; i <= 10; i++ )); do
echo $i
user="user$i"
if [ $i -eq 5 ]; then
break
fi
done
shift 用于迁移位置变量,将$1~$9 依次向左传递
tot=0
while [ $# -gt 0 ]; do
tot=((tot+$1))
shift
done
函数
function add(){
echo $(($1+$2))
}
add 11 12
find . -name '*.txt' -- search files in current dir which's extension is .txt
find . -name '*file[1-6]*.txt'
find . -perm 755 -type f
-type
c character (unbuffered) special
d directory
f regular file
find / -user user1 -- find the files create by user1 from root dir
stat ./test.txt --display file or file system status
find . -mtime -5 -- find the files which're been modified for 5 days
find . -mtime +3 -- find the files which're been modified before 3 days ago
find . -size +1000000c -- find files which's size is roughly 1M
find . -name '*file[1-6]*.txt' | xargs rm -rf --xargs 将find找到的文件作为参数传给后面的rm命令处理,因为rm删除文件时需要文件名
grep "linux" *
grep -c "linux" filename --print a count of matching lines for each input file
grep -n "linux" filename --prefix each line of output with the line number within its input file
grep -i "linux" filename --ignore-case
grep -v "linux" filename --to select non-matching lines
grep -E "^linux" filename --search the line start by linux
grep -E "linux$" filename --search the line end by linux
grep -E ".+linux.+" finename --seach the line including linux but not start and end by linux
grep -E "^$" filename --empty line
grep -E "^.$" filename --match lines which only have one character
grep -E '[0-9]+' filename --match numbers
grep -En '[0-9]+.[0-9]+.[0-9]+.[0-9]+' filename --match ip
grep -En '([0-9]+.){3}[0-9]+' filename
grep -E '2014:22:5[0-9]' filename --在文件中查找时间在2014:22:50到2014:22:59之间的行
grep -E '^[^210]' filename --在文件中查找不是以210开头的行
grep -E '^2004|^2015' filename --在文件中查找是以2004或以2015开头的行
grep -E '[6-9][0-9]' filename --查找从60到99之间的成绩
. 任意一个字符
+ 多个字符
* 零个或多个字符
[0-9a-z] 匹配[]是的任意一个
(linux)+ 匹配出现多次的单元()
\ 对正则表达示式中的特殊字符进行车转义
(unit){n} 匹配n个的单元
(unit){n,} 匹配n个以上的单元
(unit){n,x} 匹配n到x个的单元
awk -F: '{print }'
awk 默认的分隔符是空格
cat /etc/passwd | cut -d: -f1 --以:为分隔符并取出第一列
cat /etc/passwd | awk -F: '{print $1}' --以:为分隔符并取出第一列
cat /etc/passwd | awk -F: '{print "username: "$1" => Shell: "$7}' --以:为分隔符并取出第一列和第七列
cat /etc/passwd | awk -F: '{print $1}'
NR -- awk中的一个常量代表行号
NF -- awk中的一个常量代表列号
df | awk '{if(NR==4){print $0}}' --打印出第四行的整行
df | awk '{if(NR==4){print $1}}' --打印出第四行的第一列
df | awk '{if(NR==4){print int($5)}}' --将字符串转成int型方便进行整形比较
df | awk 'END{print NR}' --打印出总共的行数
df | awk 'END{print NF}' --打印出总共的列数
sed 行定位的使用
sed -n '3,5'p filename --打印出3到5行
sed '3,5'd filename --打印出除第3到5行之外的数据
cat /etc/passwd | sed -n "/bash/"p --打印出包含bash的行
cat /etc/passwd | sed -n '2,/sys:/'p --打印出第2行到包含sys:的行
cat sedtemp | sed -n '/333/,$'p --打印出包含333的行到最后一行
uniq filename --打印出过滤掉紧挨重复行的唯一值
uniq -c filename --打印出紧挨重复行出现的次数
uniq -d filename --只打印出紧挨重复的行
sort filename --升序排列
sort -r filename --降序排列
sort uniqtemp | uniq -c
cat uniqtemp | sort -t: -k2 -r --以:分隔后的第二列的倒序进行排序,默认分隔符为空格
| 创建时间: | 2020/9/2 15:26 |
| 更新时间: | 2020/9/2 15:26 |
| 作者: | Chris |
编辑/etc/vim/vimrc.tiny 使用root权限操作:
将“set compatible” 改成 “set nocompatible”
新增一条配置:
set backspace=2
最终效果是:
set nocompatible
set backspace=2
保存退出。
sudo ufw status
sudo ufw disable
sudo ufw enableufw default allow/deny:外来访问默认允许/拒绝
ufw allow/deny 20:允许/拒绝 访问20端口
查看所有服务状态
sudo service --status-all
停止启动重启服务
sudo service ssh stop|start|restart
systemctl stop|start|restart ssh
vi /etc/hostname 修改主机名称为hadoopmaster
ifconfig 获取本机IP 例如 192.168.101.131
修改主机的映射配置
vi /ect/hosts
写入192.168.101.131 hadoopmaster
保存并重启主机
如果主机名称修改不生效
root@ubuntu: vi /etc/cloud/cloud.cfg
#This will cause the set+update hostname module to not operate (if true)
preserve_hostname: false
修改为
preserve_hostname: true
apt-get install openssh-server
##start ssh
/etc/init.d/ssh start
##check if ssh started
ps -e | grep sshd
##allow root login through ssh
vi /etc/ssh/sshd_config
PermitRootLogin yes
在整个hadoop的整个处理过程中都是利用ssh进行通讯的,就算是本机也知必须配置ssh免登录处理:
由于电脑上可能出现过ssh的相关配置,因此必须删除根目录下的".ssh"的文件夹
rm -rf ~/.ssh
在hadoopmaster 主机上生成ssh key
ssh-keygen -t rsa
把生成的共钥拷贝到授权的文件之中
root@hadoopmaster:~/.ssh# cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
测试免登录处理
root@hadoopmaster:~/.ssh# ssh root@hadoopmaster
使用exit退出运程登录
Ubuntu的默认root密码是随机的,即每次开机都有一个新的root密码。
解决方法:
我们可以在终端输入命令“sudo passwd”,然后输入当前用户的密码后"Enter"。
终端会提示我们输入新的密码并确认,此时的密码就是root新密码。
修改成功后,输入命令 su root,再输入新的密码就ok了。
将当前用户注销
logout
| 创建时间: | 2020/9/2 15:21 |
| 更新时间: | 2020/9/2 15:23 |
| 作者: | Chris |
git pull & git pull --rebase
目前我的两个分支master 和 dev2018-10-17-001

git checkout dev2018-10-17-001
git pull origin master 将远程分支(master)要与当前分支合并

git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作)

git reset --hard 5306c99 //回滚到指定版本同进回滚index和working区
git pull --rebase origin master


| 创建时间: | 2020/9/2 15:07 |
| 更新时间: | 2020/9/2 15:08 |
| 作者: | Chris |
1.由object类提供
2.equals默认比较两个对象是否使用同一内存地址
3.hashcode返回随机唯一值来标识内存地址
4.相等的两个对象必须有相同的hashcode,
5.有相同hashcode的两个对象不一定相等,因为hashcode可以自定义。
6.覆盖equals方法时要同进覆盖hashcode方法,以避免在hashcode的数据结构如hashMap, HashTable, HashSet中比较对象时失败.
By default, the Java super class java.lang.Object provides two important methods for comparing objects: equals() and hashcode().
The default implementation is not enough to satisfy business needs, especially if we're talking about a huge application that considers two objects as equal when some business fact happens. In some business scenarios, developers provide their own implementation in order to force their own equality mechanism regardless the memory addresses.
As per the Java documentation, developers should override both methods in order to achieve a fully working equality mechanism — it's not enough to just implement the equals() method. If two objects are equal according to the equals(Object) method, then calling the hashcode() method on each of the two objects must produce the same integer result.
In the following sections, we provide several examples that show the importance of overriding both methods and the drawbacks of overriding equals() without hashcode().
Practical Example**
We define a class called Student as the following:
package com.programmer.gate.beans;
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.name = name;
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
For testing purposes, we define a main class HashcodeEquals that checks whether two instances of Student (who have the exact same attributes) are considered as equal.
public class HashcodeEquals {
public static void main(String[] args) {
Student alex1 = new Student(1, "Alex");
Student alex2 = new Student(1, "Alex");
System.out.println("alex1 hashcode = " + alex1.hashCode());
System.out.println("alex2 hashcode = " + alex2.hashCode());
System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
}
}
Output:
alex1 hashcode = 1852704110
alex2 hashcode = 2032578917
Checking equality between alex1 and alex2 = false
Although the two instances have exactly the same attribute values, they are stored in different memory locations. Hence, they are not considered equal as per the default implementation of equals(). The same applies for hashcode() — a random unique code is generated for each instance.
Overriding equals()
For business purposes, we consider that two students are equal if they have the same ID, so we override the equals() method and provide our own implementation as the following:
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof Student))
return false;
if (obj == this)
return true;
return this.getId() == ((Student) obj).getId();
}
In the above implementation, we are saying that two students are equal if and only if they are stored in the same memory address OR they have the same ID. Now if we try to run HashcodeEquals*,* we will get the following output:
alex1 hashcode = 2032578917
alex2 hashcode = 1531485190
Checking equality between alex1 and alex2 = true
As you noticed, overriding equals() with our custom business forces Java to consider the ID attribute when comparing two Student objects.
A very popular usage of equals() is defining an array list of Student and searching for a particular student inside it. So we modified our testing class in order the achieve this.
public class HashcodeEquals {
public static void main(String[] args) {
Student alex = new Student(1, "Alex");
List <Student> studentsLst = new ArrayList <Student> ();
studentsLst.add(alex);
System.out.println("Arraylist size = " + studentsLst.size());
System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1, "Alex")));
}
}
After running the above test, we get the following output:
Arraylist size = 1
Arraylist contains Alex = true
Okay, so we override equals() and we get the expected behavior — even though the hash code of the two objects are different. So, what's the purpose of overriding hashcode()?
equals() With HashSet
Let's consider a new test scenario. We want to store all the students in a HashSet, so we update HashcodeEquals as the following:
public class HashcodeEquals {
public static void main(String[] args) {
Student alex1 = new Student(1, "Alex");
Student alex2 = new Student(1, "Alex");
HashSet <Student> students = new HashSet <Student> ();
students.add(alex1);
students.add(alex2);
System.out.println("HashSet size = " + students.size());
System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
}
}
If we run the above test, we get the following output:
HashSet size = 2
HashSet contains Alex = false
WAIT!! We already override equals() and verify that alex1 and alex2 are equal, and we all know that HashSet stores unique objects, so why did it consider them as different objects ?
HashSet stores its elements in memory buckets. Each bucket is linked to a particular hash code.
When calling students.add(alex1), Java stores alex1 inside a bucket and links it to the value of alex1.hashcode(). Now any time an element with the same hash code is inserted into the set, it will just replace *alex1.*However, since alex2 has a different hash code, it will be stored in a separate bucket and will be considered a totally different object.
Now when HashSet searches for an element inside it, it first generates the element's hash code and looks for a bucket which corresponds to this hash code.
Here comes the importance of overriding hashcode(), so let's override it in Student and set it to be equal to the ID so that students who have the same ID are stored in the same bucket:
@Override
public int hashCode() {
return id;
}
Now if we try to run the same test, we get the following output:
HashSet size = 1
HashSet contains Alex = true
See the magic of hashcode()! The two elements are now considered as equal and stored in the same memory bucket, so any time you call contains() and pass a student object holding the same hash code, the set will be able to find the element.
The same is applied for HashMap, HashTable, or any data structure that uses a hashing mechanism for storing elements.
In order to achieve a fully working custom equality mechanism, it is mandatory to override hashcode() each time you override equals(). Follow the tips below and you'll never have leaks in your custom equality mechanism:
- If two objects are equal, they MUST have the same hash code.
- If two objects have the same hash code, it doesn't mean that they are equal.
- Overriding equals() alone will make your business fail with hashing data structures like: HashSet, HashMap, HashTable ... etc.
- Overriding hashcode() alone doesn't force Java to ignore memory addresses when comparing two objects.
| 创建时间: | 2020/9/2 14:39 |
| 更新时间: | 2020/9/2 14:42 |
| 作者: | Chris |
docker logs es2
删除容器
docker rm es2
重新创建容器ES_JAVA_OPTS=-Xms512m -Xmx512m
docker run -d --name es2 -e ES_JAVA_OPTS=-Xms512m -Xmx512m -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" 5c1e1ecfe33a
ElasticSearch默认的cluster.name为elasticsearch,所以,这里需要进行修改。
vi /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "qfcwx-cluster"
network.host: 0.0.0.0
http.cors.enabled: true
http.cors.allow-origin: "*"
cluster.name:自定义集群名称。
network.host:当前es节点绑定的ip地址,默认127.0.0.1,如果需要开放对外访问这个属性必须设置。
http.cors.enabled:是否支持跨域,默认为false。
http.cors.allow-origin:当设置允许跨域,默认为*,表示支持所有域名,如果我们只是允许某些网站能访问,那么可以使用正则表达式。
exit
docker restart es2
http://master:9200/

To make it persistent, you can add this line:
vm.max_map_count=262144
in your /etc/sysctl.conf and run
sudo sysctl -p
docker inspect image_id

Ctrl+Alt+M
可以正向操作也可以反向操作


Shift+F6
Ctrl+Alt+C 快速提取常量(Constant)
Ctrl+Alt+V 快速提取变量(Variable)
Ctrl+Alt+F 快速提取成员变量(Filed Variable)
Ctrl+Shift+f6 重构变量的类型




通过显示内存使用情况来查看当前项目占用的内存大小
选择顶部导航栏中的Help,然后点击Edit Custom VM Options
-Xmx1024m // 最大内存上限为:1024MB(1GB)
-Xms256m // 初始内存分配大小为:256MB
-XX:ReservedCodeCacheSize=128m //代码缓冲区大小:128MB
-XX:+UseG1GC
-Xmx4096m
-Xms4096m
-XX:ReservedCodeCacheSize=256m
-XX:+UseG1GC
参数说明
-server:一定要作为第一个参数,在多个CPU时性能佳
-Xms:初始Heap大小,使用的最小内存,cpu性能高时此值应设的大一些
-Xmx:java heap最大值,使用的最大内存
-XX:PermSize:设定内存的永久保存区域
-XX:MaxPermSize:设定最大内存的永久保存区域
-XX:MaxNewSize:
+XX:AggressiveHeap 使 Xms 失去意义。
-Xss:每个线程的Stack大小
-verbose:gc 现实垃圾收集信息
-Xloggc:gc.log 指定垃圾收集日志文件
-Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一
-XX:+UseParNewGC :缩短minor收集的时间
-XX:+UseConcMarkSweepGC :缩短major收集的时间
提示:此选项在Heap Size 比较大而且Major收集时间较长的情况下使用更合适。
